wbcommission 2.2.1__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 wbcommission might be problematic. Click here for more details.
- wbcommission/__init__.py +1 -0
- wbcommission/admin/__init__.py +4 -0
- wbcommission/admin/accounts.py +22 -0
- wbcommission/admin/commission.py +85 -0
- wbcommission/admin/rebate.py +7 -0
- wbcommission/analytics/__init__.py +0 -0
- wbcommission/analytics/marginality.py +181 -0
- wbcommission/apps.py +5 -0
- wbcommission/dynamic_preferences_registry.py +0 -0
- wbcommission/factories/__init__.py +9 -0
- wbcommission/factories/commission.py +100 -0
- wbcommission/factories/rebate.py +16 -0
- wbcommission/filters/__init__.py +7 -0
- wbcommission/filters/rebate.py +187 -0
- wbcommission/filters/signals.py +44 -0
- wbcommission/generators/__init__.py +2 -0
- wbcommission/generators/rebate_generator.py +93 -0
- wbcommission/migrations/0001_initial.py +299 -0
- wbcommission/migrations/0002_commissionrule_remove_accountcustomer_account_and_more.py +395 -0
- wbcommission/migrations/0003_alter_commission_account.py +24 -0
- wbcommission/migrations/0004_rebate_audit_log.py +19 -0
- wbcommission/migrations/0005_alter_rebate_audit_log.py +20 -0
- wbcommission/migrations/0006_commissionrule_consider_zero_percent_for_exclusion.py +21 -0
- wbcommission/migrations/0007_remove_commission_unique_crm_recipient_account_and_more.py +50 -0
- wbcommission/migrations/0008_alter_commission_options_alter_commission_order.py +26 -0
- wbcommission/migrations/__init__.py +0 -0
- wbcommission/models/__init__.py +9 -0
- wbcommission/models/account_service.py +217 -0
- wbcommission/models/commission.py +679 -0
- wbcommission/models/rebate.py +319 -0
- wbcommission/models/signals.py +45 -0
- wbcommission/permissions.py +6 -0
- wbcommission/reports/__init__.py +0 -0
- wbcommission/reports/audit_report.py +51 -0
- wbcommission/reports/customer_report.py +299 -0
- wbcommission/reports/utils.py +30 -0
- wbcommission/serializers/__init__.py +3 -0
- wbcommission/serializers/commissions.py +26 -0
- wbcommission/serializers/rebate.py +87 -0
- wbcommission/serializers/signals.py +27 -0
- wbcommission/tests/__init__.py +0 -0
- wbcommission/tests/analytics/__init__.py +0 -0
- wbcommission/tests/analytics/test_marginality.py +253 -0
- wbcommission/tests/conftest.py +89 -0
- wbcommission/tests/models/__init__.py +0 -0
- wbcommission/tests/models/mixins.py +22 -0
- wbcommission/tests/models/test_account_service.py +293 -0
- wbcommission/tests/models/test_commission.py +587 -0
- wbcommission/tests/models/test_rebate.py +136 -0
- wbcommission/tests/signals.py +0 -0
- wbcommission/tests/test_permissions.py +66 -0
- wbcommission/tests/viewsets/__init__.py +0 -0
- wbcommission/tests/viewsets/test_rebate.py +76 -0
- wbcommission/urls.py +42 -0
- wbcommission/viewsets/__init__.py +7 -0
- wbcommission/viewsets/buttons/__init__.py +2 -0
- wbcommission/viewsets/buttons/rebate.py +46 -0
- wbcommission/viewsets/buttons/signals.py +53 -0
- wbcommission/viewsets/commissions.py +21 -0
- wbcommission/viewsets/display/__init__.py +5 -0
- wbcommission/viewsets/display/commissions.py +21 -0
- wbcommission/viewsets/display/rebate.py +117 -0
- wbcommission/viewsets/endpoints/__init__.py +4 -0
- wbcommission/viewsets/endpoints/commissions.py +0 -0
- wbcommission/viewsets/endpoints/rebate.py +21 -0
- wbcommission/viewsets/menu/__init__.py +1 -0
- wbcommission/viewsets/menu/commissions.py +0 -0
- wbcommission/viewsets/menu/rebate.py +13 -0
- wbcommission/viewsets/mixins.py +39 -0
- wbcommission/viewsets/rebate.py +481 -0
- wbcommission/viewsets/titles/__init__.py +1 -0
- wbcommission/viewsets/titles/commissions.py +0 -0
- wbcommission/viewsets/titles/rebate.py +11 -0
- wbcommission-2.2.1.dist-info/METADATA +11 -0
- wbcommission-2.2.1.dist-info/RECORD +76 -0
- wbcommission-2.2.1.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from faker import Faker
|
|
3
|
+
from pandas.tseries.offsets import BDay
|
|
4
|
+
from rest_framework.reverse import reverse
|
|
5
|
+
from rest_framework.test import APIClient, APIRequestFactory
|
|
6
|
+
from wbcommission.analytics.marginality import MarginalityCalculator
|
|
7
|
+
from wbcommission.factories import CommissionTypeFactory, RebateFactory
|
|
8
|
+
from wbcommission.viewsets.rebate import RebateProductMarginalityViewSet
|
|
9
|
+
from wbportfolio.factories import FeesFactory, InstrumentPriceFactory, ProductFactory
|
|
10
|
+
from wbportfolio.models import Product
|
|
11
|
+
|
|
12
|
+
fake = Faker()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _create_fixture(product, val_date, net_value=100, outstanding_shares=100):
|
|
16
|
+
management = CommissionTypeFactory.create(key="management")
|
|
17
|
+
perforance = CommissionTypeFactory.create(key="performance")
|
|
18
|
+
i1 = InstrumentPriceFactory.create(
|
|
19
|
+
instrument=product, net_value=net_value, outstanding_shares=outstanding_shares, calculated=False, date=val_date
|
|
20
|
+
).net_value # create a price of AUM 100*100
|
|
21
|
+
management_fees_1 = FeesFactory.create(
|
|
22
|
+
linked_product=product, transaction_date=val_date, transaction_subtype="MANAGEMENT", calculated=False
|
|
23
|
+
).total_value
|
|
24
|
+
performance_fees_1 = FeesFactory.create(
|
|
25
|
+
linked_product=product, transaction_date=val_date, transaction_subtype="PERFORMANCE", calculated=False
|
|
26
|
+
).total_value
|
|
27
|
+
performance_crys_fees_1 = FeesFactory.create(
|
|
28
|
+
linked_product=product,
|
|
29
|
+
transaction_date=val_date,
|
|
30
|
+
transaction_subtype="PERFORMANCE_CRYSTALIZED",
|
|
31
|
+
calculated=False,
|
|
32
|
+
).total_value
|
|
33
|
+
management_rebate_1 = RebateFactory.create(product=product, date=val_date, commission_type=management).value
|
|
34
|
+
performance_rebate_1 = RebateFactory.create(product=product, date=val_date, commission_type=perforance).value
|
|
35
|
+
if val_date.weekday() == 0:
|
|
36
|
+
return (
|
|
37
|
+
i1,
|
|
38
|
+
management_fees_1 / 3.0,
|
|
39
|
+
performance_fees_1 / 3.0,
|
|
40
|
+
performance_crys_fees_1 / 3.0,
|
|
41
|
+
float(management_rebate_1) / 3.0,
|
|
42
|
+
float(performance_rebate_1) / 3.0,
|
|
43
|
+
)
|
|
44
|
+
else:
|
|
45
|
+
return (
|
|
46
|
+
i1,
|
|
47
|
+
management_fees_1,
|
|
48
|
+
performance_fees_1,
|
|
49
|
+
performance_crys_fees_1,
|
|
50
|
+
float(management_rebate_1),
|
|
51
|
+
float(performance_rebate_1),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.mark.django_db
|
|
56
|
+
class TestMarginalityCalculator:
|
|
57
|
+
@pytest.fixture
|
|
58
|
+
def marginality_calculator(self):
|
|
59
|
+
product = ProductFactory.create()
|
|
60
|
+
start = (fake.date_object() + BDay(0)).date()
|
|
61
|
+
end = (start + BDay(1)).date()
|
|
62
|
+
|
|
63
|
+
(
|
|
64
|
+
self.i1,
|
|
65
|
+
self.management_fees_1,
|
|
66
|
+
self.performance_fees_1,
|
|
67
|
+
self.performance_crys_fees_1,
|
|
68
|
+
self.management_rebate_1,
|
|
69
|
+
self.performance_rebate_1,
|
|
70
|
+
) = _create_fixture(product, start)
|
|
71
|
+
self.product_id = product.id
|
|
72
|
+
self.start = start
|
|
73
|
+
self.end = end
|
|
74
|
+
return MarginalityCalculator(Product.objects.filter(id=product.id), start, end)
|
|
75
|
+
|
|
76
|
+
def test_fees(self, marginality_calculator):
|
|
77
|
+
assert marginality_calculator.management_fees.loc[self.product_id] == pytest.approx(
|
|
78
|
+
self.management_fees_1, rel=1e-4
|
|
79
|
+
)
|
|
80
|
+
assert marginality_calculator.performance_fees.loc[self.product_id] == pytest.approx(
|
|
81
|
+
self.performance_fees_1 + self.performance_crys_fees_1, rel=1e-4
|
|
82
|
+
)
|
|
83
|
+
assert marginality_calculator.total_fees.loc[self.product_id] == pytest.approx(
|
|
84
|
+
self.performance_fees_1 + self.performance_crys_fees_1 + self.management_fees_1, rel=1e-4
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def test_rebates(self, marginality_calculator):
|
|
88
|
+
assert marginality_calculator.management_rebates.loc[self.product_id] == pytest.approx(
|
|
89
|
+
self.management_rebate_1, rel=1e-4
|
|
90
|
+
)
|
|
91
|
+
assert marginality_calculator.performance_rebates.loc[self.product_id] == pytest.approx(
|
|
92
|
+
self.performance_rebate_1, rel=1e-4
|
|
93
|
+
)
|
|
94
|
+
assert marginality_calculator.total_rebates.loc[self.product_id] == pytest.approx(
|
|
95
|
+
self.performance_rebate_1 + self.management_rebate_1, rel=1e-4
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def test_fees_usd(self, marginality_calculator):
|
|
99
|
+
assert marginality_calculator.management_fees_usd.loc[self.product_id] == pytest.approx(
|
|
100
|
+
self.management_fees_1, rel=1e-4
|
|
101
|
+
)
|
|
102
|
+
assert marginality_calculator.performance_fees_usd.loc[self.product_id] == pytest.approx(
|
|
103
|
+
self.performance_fees_1 + self.performance_crys_fees_1, rel=1e-4
|
|
104
|
+
)
|
|
105
|
+
assert marginality_calculator.total_fees_usd.loc[self.product_id] == pytest.approx(
|
|
106
|
+
self.performance_fees_1 + self.performance_crys_fees_1 + self.management_fees_1, rel=1e-4
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def test_rebates_usd(self, marginality_calculator):
|
|
110
|
+
assert marginality_calculator.management_rebates_usd.loc[self.product_id] == pytest.approx(
|
|
111
|
+
self.management_rebate_1, rel=1e-4
|
|
112
|
+
)
|
|
113
|
+
assert marginality_calculator.performance_rebates_usd.loc[self.product_id] == pytest.approx(
|
|
114
|
+
self.performance_rebate_1, rel=1e-4
|
|
115
|
+
)
|
|
116
|
+
assert marginality_calculator.total_rebates_usd.loc[self.product_id] == pytest.approx(
|
|
117
|
+
self.performance_rebate_1 + self.management_rebate_1, rel=1e-4
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def test_marginality(self, marginality_calculator):
|
|
121
|
+
assert marginality_calculator.management_marginality.loc[self.product_id] == pytest.approx(
|
|
122
|
+
self.management_fees_1 - self.management_rebate_1, rel=1e-4
|
|
123
|
+
)
|
|
124
|
+
assert marginality_calculator.performance_marginality.loc[self.product_id] == pytest.approx(
|
|
125
|
+
self.performance_fees_1 + self.performance_crys_fees_1 - self.performance_rebate_1, rel=1e-4
|
|
126
|
+
)
|
|
127
|
+
assert marginality_calculator.total_marginality.loc[self.product_id] == pytest.approx(
|
|
128
|
+
(self.performance_fees_1 + self.performance_crys_fees_1 + self.management_fees_1)
|
|
129
|
+
- (self.performance_rebate_1 + self.management_rebate_1),
|
|
130
|
+
rel=1e-4,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def test_marginality_usd(self, marginality_calculator):
|
|
134
|
+
assert marginality_calculator.management_marginality_usd.loc[self.product_id] == pytest.approx(
|
|
135
|
+
marginality_calculator.management_marginality.loc[self.product_id], rel=1e-4
|
|
136
|
+
)
|
|
137
|
+
assert marginality_calculator.performance_marginality_usd.loc[self.product_id] == pytest.approx(
|
|
138
|
+
marginality_calculator.performance_marginality.loc[self.product_id], rel=1e-4
|
|
139
|
+
)
|
|
140
|
+
assert marginality_calculator.total_marginality_usd.loc[self.product_id] == pytest.approx(
|
|
141
|
+
marginality_calculator.total_marginality.loc[self.product_id], rel=1e-4
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def test_marginality_percent(self, marginality_calculator):
|
|
145
|
+
assert marginality_calculator.management_marginality_percent.loc[self.product_id] == pytest.approx(
|
|
146
|
+
(self.management_fees_1 - float(self.management_rebate_1)) / self.management_fees_1, rel=1e-4
|
|
147
|
+
)
|
|
148
|
+
assert marginality_calculator.performance_marginality_percent.loc[self.product_id] == pytest.approx(
|
|
149
|
+
(self.performance_fees_1 + self.performance_crys_fees_1 - float(self.performance_rebate_1))
|
|
150
|
+
/ (self.performance_fees_1 + self.performance_crys_fees_1),
|
|
151
|
+
rel=1e-4,
|
|
152
|
+
)
|
|
153
|
+
assert marginality_calculator.total_marginality_percent.loc[self.product_id] == pytest.approx(
|
|
154
|
+
(
|
|
155
|
+
(self.performance_fees_1 + self.performance_crys_fees_1 + self.management_fees_1)
|
|
156
|
+
- (float(self.performance_rebate_1) + float(self.management_rebate_1))
|
|
157
|
+
)
|
|
158
|
+
/ (self.performance_fees_1 + self.performance_crys_fees_1 + self.management_fees_1),
|
|
159
|
+
rel=1e-4,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def test_marginality_percent_usd(self, marginality_calculator):
|
|
163
|
+
assert marginality_calculator.management_marginality_percent_usd.loc[self.product_id] == pytest.approx(
|
|
164
|
+
marginality_calculator.management_marginality_percent.loc[self.product_id], rel=1e-4
|
|
165
|
+
)
|
|
166
|
+
assert marginality_calculator.performance_marginality_percent_usd.loc[self.product_id] == pytest.approx(
|
|
167
|
+
marginality_calculator.performance_marginality_percent.loc[self.product_id], rel=1e-4
|
|
168
|
+
)
|
|
169
|
+
assert marginality_calculator.total_marginality_percent_usd.loc[self.product_id] == pytest.approx(
|
|
170
|
+
marginality_calculator.total_marginality_percent.loc[self.product_id], rel=1e-4
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def test_get_net_marginality(self, marginality_calculator):
|
|
174
|
+
assert marginality_calculator.get_net_marginality("management").loc[self.product_id] == pytest.approx(
|
|
175
|
+
(self.management_fees_1 - float(self.management_rebate_1)) / (100 * 100) * 360, rel=1e-4
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def test_get_aggregated_net_marginality(self, marginality_calculator):
|
|
179
|
+
product_1 = ProductFactory.create()
|
|
180
|
+
product_2 = ProductFactory.create()
|
|
181
|
+
start = (fake.date_object() + BDay(0)).date()
|
|
182
|
+
end = (start + BDay(1)).date()
|
|
183
|
+
|
|
184
|
+
(
|
|
185
|
+
i1,
|
|
186
|
+
management_fees_1,
|
|
187
|
+
performance_fees_1,
|
|
188
|
+
performance_crys_fees_1,
|
|
189
|
+
management_rebate_1,
|
|
190
|
+
performance_rebate_1,
|
|
191
|
+
) = _create_fixture(product_1, start)
|
|
192
|
+
(
|
|
193
|
+
i2,
|
|
194
|
+
management_fees_2,
|
|
195
|
+
performance_fees_2,
|
|
196
|
+
performance_crys_fees_2,
|
|
197
|
+
management_rebate_2,
|
|
198
|
+
performance_rebate_2,
|
|
199
|
+
) = _create_fixture(product_2, start, outstanding_shares=1000)
|
|
200
|
+
total_aum = 100 * 100 + 100 * 1000
|
|
201
|
+
calculator = MarginalityCalculator(Product.objects.filter(id__in=[product_1.id, product_2.id]), start, end)
|
|
202
|
+
assert calculator.get_aggregated_net_marginality("management") == pytest.approx(
|
|
203
|
+
((management_fees_1 + management_fees_2) - (management_rebate_1 + management_rebate_2)) / total_aum * 360,
|
|
204
|
+
rel=1e-4,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def test_rebate_marginality_view(self, marginality_calculator, super_user):
|
|
208
|
+
request = APIRequestFactory().get("")
|
|
209
|
+
request.query_params = {}
|
|
210
|
+
request.GET = {"date_gte": self.start.strftime("%Y-%m-%d"), "date_lte": self.end.strftime("%Y-%m-%d")}
|
|
211
|
+
request.user = super_user
|
|
212
|
+
viewset = RebateProductMarginalityViewSet(request=request)
|
|
213
|
+
assert set(viewset._get_dataframe().columns) == {
|
|
214
|
+
"id",
|
|
215
|
+
"title",
|
|
216
|
+
"currency_symbol",
|
|
217
|
+
"base_management_fees_percent",
|
|
218
|
+
"management_fees",
|
|
219
|
+
"management_rebates",
|
|
220
|
+
"management_marginality",
|
|
221
|
+
"management_marginality_percent",
|
|
222
|
+
"base_performance_fees_percent",
|
|
223
|
+
"performance_fees",
|
|
224
|
+
"performance_rebates",
|
|
225
|
+
"performance_marginality",
|
|
226
|
+
"total_fees",
|
|
227
|
+
"total_rebates",
|
|
228
|
+
"total_marginality_percent",
|
|
229
|
+
"total_fees_usd",
|
|
230
|
+
"total_rebates_usd",
|
|
231
|
+
"total_marginality_usd",
|
|
232
|
+
"net_management_marginality",
|
|
233
|
+
"net_performance_marginality",
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# regression tests to check that without date range the view doesn't break
|
|
237
|
+
url = reverse("wbcommission:rebatemarginalitytable-list", args=[])
|
|
238
|
+
api_client = APIClient()
|
|
239
|
+
api_client.force_authenticate(super_user)
|
|
240
|
+
|
|
241
|
+
response = api_client.options(url)
|
|
242
|
+
assert response.status_code == 200
|
|
243
|
+
|
|
244
|
+
response = api_client.get(url)
|
|
245
|
+
assert response.status_code == 200
|
|
246
|
+
|
|
247
|
+
# check that with proper date range the viewset return some content
|
|
248
|
+
response = api_client.options(url, data=request.GET)
|
|
249
|
+
assert response.status_code == 200
|
|
250
|
+
|
|
251
|
+
response = api_client.get(url, data=request.GET)
|
|
252
|
+
assert response.status_code == 200
|
|
253
|
+
assert response.json()["results"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from wbcore.tests.conftest import * # isort: skip # type: ignore
|
|
2
|
+
from django.apps import apps
|
|
3
|
+
from django.db.models.signals import pre_migrate
|
|
4
|
+
from pytest_factoryboy import register
|
|
5
|
+
from wbcore.contrib.authentication.factories import (
|
|
6
|
+
AuthenticatedPersonFactory,
|
|
7
|
+
SuperUserFactory,
|
|
8
|
+
UserFactory,
|
|
9
|
+
)
|
|
10
|
+
from wbcore.contrib.currency.factories import CurrencyFactory, CurrencyFXRatesFactory
|
|
11
|
+
from wbcore.contrib.directory.factories.entries import (
|
|
12
|
+
CompanyFactory,
|
|
13
|
+
CompanyTypeFactory,
|
|
14
|
+
CustomerStatusFactory,
|
|
15
|
+
EntryFactory,
|
|
16
|
+
PersonFactory,
|
|
17
|
+
)
|
|
18
|
+
from wbcore.contrib.geography.factories import (
|
|
19
|
+
CityFactory,
|
|
20
|
+
ContinentFactory,
|
|
21
|
+
CountryFactory,
|
|
22
|
+
StateFactory,
|
|
23
|
+
)
|
|
24
|
+
from wbcore.contrib.geography.tests.signals import app_pre_migration
|
|
25
|
+
from wbcrm.factories import AccountFactory, AccountRoleFactory, AccountRoleTypeFactory
|
|
26
|
+
from wbfdm.factories import ExchangeFactory, InstrumentFactory, InstrumentTypeFactory
|
|
27
|
+
from wbportfolio.factories import (
|
|
28
|
+
ClaimFactory,
|
|
29
|
+
CustomerTradeFactory,
|
|
30
|
+
FeesFactory,
|
|
31
|
+
InstrumentPriceFactory,
|
|
32
|
+
PortfolioFactory,
|
|
33
|
+
ProductFactory,
|
|
34
|
+
ProductPortfolioRoleFactory,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from ..factories import (
|
|
38
|
+
AccountTypeRoleCommissionFactory,
|
|
39
|
+
CommissionExclusionRuleFactory,
|
|
40
|
+
CommissionFactory,
|
|
41
|
+
CommissionRoleFactory,
|
|
42
|
+
CommissionTypeFactory,
|
|
43
|
+
PortfolioRoleCommissionFactory,
|
|
44
|
+
RebateFactory,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
register(AccountFactory)
|
|
48
|
+
register(AccountRoleFactory)
|
|
49
|
+
register(AccountRoleTypeFactory)
|
|
50
|
+
|
|
51
|
+
register(InstrumentFactory)
|
|
52
|
+
register(InstrumentTypeFactory)
|
|
53
|
+
register(ExchangeFactory)
|
|
54
|
+
register(ProductFactory)
|
|
55
|
+
register(ProductPortfolioRoleFactory)
|
|
56
|
+
register(FeesFactory)
|
|
57
|
+
register(PortfolioFactory)
|
|
58
|
+
register(InstrumentPriceFactory)
|
|
59
|
+
register(ClaimFactory)
|
|
60
|
+
register(CustomerTradeFactory)
|
|
61
|
+
register(CurrencyFXRatesFactory)
|
|
62
|
+
|
|
63
|
+
register(CurrencyFactory)
|
|
64
|
+
register(CityFactory)
|
|
65
|
+
register(StateFactory)
|
|
66
|
+
register(CountryFactory)
|
|
67
|
+
register(ContinentFactory)
|
|
68
|
+
|
|
69
|
+
register(CompanyFactory)
|
|
70
|
+
register(EntryFactory)
|
|
71
|
+
register(PersonFactory)
|
|
72
|
+
register(CustomerStatusFactory)
|
|
73
|
+
register(CompanyTypeFactory)
|
|
74
|
+
|
|
75
|
+
register(AuthenticatedPersonFactory, "authenticated_person")
|
|
76
|
+
register(UserFactory)
|
|
77
|
+
register(SuperUserFactory, "superuser")
|
|
78
|
+
|
|
79
|
+
register(RebateFactory)
|
|
80
|
+
register(CommissionTypeFactory)
|
|
81
|
+
register(CommissionFactory)
|
|
82
|
+
register(CommissionExclusionRuleFactory)
|
|
83
|
+
register(AccountTypeRoleCommissionFactory, "account_role_type_commission")
|
|
84
|
+
register(CommissionRoleFactory)
|
|
85
|
+
register(PortfolioRoleCommissionFactory, "portfolio_role_commission")
|
|
86
|
+
from .signals import *
|
|
87
|
+
|
|
88
|
+
pre_migrate.connect(app_pre_migration, sender=apps.get_app_config("wbportfolio"))
|
|
89
|
+
from .signals import * # noqa: F401
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from wbcommission.factories import CommissionTypeFactory
|
|
3
|
+
from wbcommission.models.account_service import AccountRebateManager
|
|
4
|
+
from wbcrm.factories import AccountFactory
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AccountManagerFixture:
|
|
8
|
+
@pytest.fixture()
|
|
9
|
+
def performance_account_manager(self):
|
|
10
|
+
commission_type = CommissionTypeFactory.create(name="PERFORMANCE")
|
|
11
|
+
account_manager = AccountRebateManager(AccountFactory.create(), commission_type.key)
|
|
12
|
+
account_manager.initialize()
|
|
13
|
+
|
|
14
|
+
return account_manager
|
|
15
|
+
|
|
16
|
+
@pytest.fixture()
|
|
17
|
+
def management_account_manager(self):
|
|
18
|
+
commission_type = CommissionTypeFactory.create(name="MANAGEMENT")
|
|
19
|
+
account_manager = AccountRebateManager(AccountFactory.create(), commission_type.key)
|
|
20
|
+
account_manager.initialize()
|
|
21
|
+
|
|
22
|
+
return account_manager
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from faker import Faker
|
|
6
|
+
from pandas.tseries.offsets import BDay
|
|
7
|
+
from wbcommission.models.account_service import AccountRebateManager
|
|
8
|
+
from wbportfolio.models import Claim
|
|
9
|
+
|
|
10
|
+
from .mixins import AccountManagerFixture
|
|
11
|
+
|
|
12
|
+
fake = Faker()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.django_db
|
|
16
|
+
class TestAccountService(AccountManagerFixture):
|
|
17
|
+
def test_get_commission_pool(
|
|
18
|
+
self, management_account_manager, fees_factory, claim_factory, customer_trade_factory
|
|
19
|
+
):
|
|
20
|
+
mngt_fees = fees_factory.create(transaction_subtype="MANAGEMENT")
|
|
21
|
+
perf_fees = fees_factory.create( # noqa
|
|
22
|
+
transaction_subtype="PERFORMANCE",
|
|
23
|
+
transaction_date=mngt_fees.transaction_date,
|
|
24
|
+
linked_product=mngt_fees.linked_product,
|
|
25
|
+
) # noqa
|
|
26
|
+
claim_factory.create(
|
|
27
|
+
account=management_account_manager.root_account,
|
|
28
|
+
trade=customer_trade_factory.create(underlying_instrument=mngt_fees.linked_product),
|
|
29
|
+
status="APPROVED",
|
|
30
|
+
)
|
|
31
|
+
management_account_manager.initialize()
|
|
32
|
+
|
|
33
|
+
assert (
|
|
34
|
+
management_account_manager.get_commission_pool(mngt_fees.linked_product, mngt_fees.transaction_date)
|
|
35
|
+
== mngt_fees.total_value
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def test_get_commission_pool_performance(
|
|
39
|
+
self, performance_account_manager, fees_factory, claim_factory, customer_trade_factory
|
|
40
|
+
):
|
|
41
|
+
mngt_fees = fees_factory.create(transaction_subtype="MANAGEMENT")
|
|
42
|
+
perf_fees = fees_factory.create(
|
|
43
|
+
transaction_subtype="PERFORMANCE",
|
|
44
|
+
transaction_date=mngt_fees.transaction_date,
|
|
45
|
+
linked_product=mngt_fees.linked_product,
|
|
46
|
+
)
|
|
47
|
+
perf2_fees = fees_factory.create(
|
|
48
|
+
transaction_subtype="PERFORMANCE_CRYSTALIZED",
|
|
49
|
+
transaction_date=mngt_fees.transaction_date,
|
|
50
|
+
linked_product=mngt_fees.linked_product,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
claim_factory.create(
|
|
54
|
+
account=performance_account_manager.root_account,
|
|
55
|
+
trade=customer_trade_factory.create(underlying_instrument=mngt_fees.linked_product),
|
|
56
|
+
status="APPROVED",
|
|
57
|
+
)
|
|
58
|
+
performance_account_manager.initialize()
|
|
59
|
+
|
|
60
|
+
assert (
|
|
61
|
+
performance_account_manager.get_commission_pool(mngt_fees.linked_product, mngt_fees.transaction_date)
|
|
62
|
+
== perf_fees.total_value + perf2_fees.total_value
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def test_get_commission_pool_with_calculated(
|
|
66
|
+
self, management_account_manager, fees_factory, claim_factory, customer_trade_factory
|
|
67
|
+
):
|
|
68
|
+
# check that in case there is only calculate, we return the calculated fees value, but in case there is calculated and real fees, we use the real fees value
|
|
69
|
+
calculated_fees = fees_factory.create(calculated=True, transaction_subtype="MANAGEMENT")
|
|
70
|
+
claim_factory.create(
|
|
71
|
+
account=management_account_manager.root_account,
|
|
72
|
+
trade=customer_trade_factory.create(underlying_instrument=calculated_fees.linked_product),
|
|
73
|
+
status="APPROVED",
|
|
74
|
+
)
|
|
75
|
+
management_account_manager.initialize()
|
|
76
|
+
|
|
77
|
+
assert (
|
|
78
|
+
management_account_manager.get_commission_pool(
|
|
79
|
+
calculated_fees.linked_product, calculated_fees.transaction_date
|
|
80
|
+
)
|
|
81
|
+
== calculated_fees.total_value
|
|
82
|
+
)
|
|
83
|
+
# Check that is there is a non calculated fees, it is used instead of the calculated one
|
|
84
|
+
fees = fees_factory.create(transaction_subtype=calculated_fees.transaction_subtype, calculated=False)
|
|
85
|
+
claim_factory.create(
|
|
86
|
+
account=management_account_manager.root_account,
|
|
87
|
+
trade=customer_trade_factory.create(underlying_instrument=fees.linked_product),
|
|
88
|
+
status="APPROVED",
|
|
89
|
+
)
|
|
90
|
+
management_account_manager.initialize()
|
|
91
|
+
|
|
92
|
+
assert (
|
|
93
|
+
management_account_manager.get_commission_pool(fees.linked_product, fees.transaction_date)
|
|
94
|
+
== fees.total_value
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
@pytest.mark.parametrize("val_date", [fake.past_date()])
|
|
98
|
+
def test_get_terminal_account_holding_ratio(
|
|
99
|
+
self, account_factory, claim_factory, instrument_price_factory, product_factory, val_date, commission_type
|
|
100
|
+
):
|
|
101
|
+
product1 = product_factory.create()
|
|
102
|
+
product2 = product_factory.create()
|
|
103
|
+
val_date = (val_date + BDay(0)).date()
|
|
104
|
+
next_val_date = (val_date + BDay(1)).date()
|
|
105
|
+
|
|
106
|
+
product1_price_val_date = instrument_price_factory.create(
|
|
107
|
+
date=val_date, instrument=product1, outstanding_shares=Decimal(2e4), calculated=True
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
product1_price_next_val_date = instrument_price_factory.create(
|
|
111
|
+
date=next_val_date, instrument=product1, outstanding_shares=Decimal(2e4), calculated=True
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
product2_price_val_date = instrument_price_factory.create(
|
|
115
|
+
date=val_date, instrument=product2, outstanding_shares=Decimal(2e4), calculated=True
|
|
116
|
+
)
|
|
117
|
+
product2_price_next_val_date = instrument_price_factory.create(
|
|
118
|
+
date=next_val_date, instrument=product2, outstanding_shares=Decimal(2e4), calculated=True
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
parent_account = account_factory.create(is_terminal_account=False, is_active=True)
|
|
122
|
+
claim_factory.create(
|
|
123
|
+
trade__value_date=val_date,
|
|
124
|
+
trade__transaction_date=(val_date - BDay(1)).date(),
|
|
125
|
+
trade__underlying_instrument=product1,
|
|
126
|
+
status=Claim.Status.APPROVED,
|
|
127
|
+
account=parent_account,
|
|
128
|
+
) # ignore this claim because it's attached to a non terminal account
|
|
129
|
+
|
|
130
|
+
child_terminal_account = account_factory.create(
|
|
131
|
+
parent=parent_account, is_terminal_account=True, is_active=True
|
|
132
|
+
)
|
|
133
|
+
claim_factory.create( # ignore this claim because it's not approved
|
|
134
|
+
trade__value_date=val_date,
|
|
135
|
+
trade__transaction_date=(val_date - BDay(1)).date(),
|
|
136
|
+
account=child_terminal_account,
|
|
137
|
+
trade__underlying_instrument=product1,
|
|
138
|
+
status=random.choice([Claim.Status.DRAFT, Claim.Status.WITHDRAWN, Claim.Status.PENDING]),
|
|
139
|
+
)
|
|
140
|
+
unvalid_child_terminal_account = account_factory.create(parent=parent_account, is_active=False)
|
|
141
|
+
claim_factory.create(
|
|
142
|
+
trade__value_date=val_date,
|
|
143
|
+
trade__transaction_date=(val_date - BDay(1)).date(),
|
|
144
|
+
trade__underlying_instrument=product1,
|
|
145
|
+
status=Claim.Status.APPROVED,
|
|
146
|
+
account=unvalid_child_terminal_account,
|
|
147
|
+
) # ignore this claim as the attached account is not active
|
|
148
|
+
|
|
149
|
+
valid_claim1 = claim_factory.create(
|
|
150
|
+
trade__value_date=val_date,
|
|
151
|
+
trade__transaction_date=(val_date - BDay(1)).date(),
|
|
152
|
+
trade__underlying_instrument=product1,
|
|
153
|
+
status=Claim.Status.APPROVED,
|
|
154
|
+
account=child_terminal_account,
|
|
155
|
+
)
|
|
156
|
+
valid_claim2 = claim_factory.create(
|
|
157
|
+
trade__value_date=next_val_date,
|
|
158
|
+
trade__transaction_date=(next_val_date - BDay(1)).date(),
|
|
159
|
+
trade__underlying_instrument=product1,
|
|
160
|
+
status=Claim.Status.APPROVED,
|
|
161
|
+
account=child_terminal_account,
|
|
162
|
+
)
|
|
163
|
+
valid_claim3 = claim_factory.create(
|
|
164
|
+
trade__value_date=val_date,
|
|
165
|
+
trade__transaction_date=(val_date - BDay(1)).date(),
|
|
166
|
+
trade__underlying_instrument=product2,
|
|
167
|
+
status=Claim.Status.APPROVED,
|
|
168
|
+
account=child_terminal_account,
|
|
169
|
+
)
|
|
170
|
+
product1_price_val_date.refresh_from_db()
|
|
171
|
+
product1_price_next_val_date.refresh_from_db()
|
|
172
|
+
product2_price_val_date.refresh_from_db()
|
|
173
|
+
product2_price_next_val_date.refresh_from_db()
|
|
174
|
+
|
|
175
|
+
account_manager = AccountRebateManager(parent_account, commission_type.key)
|
|
176
|
+
account_manager.initialize()
|
|
177
|
+
assert account_manager.get_terminal_account_holding_ratio(
|
|
178
|
+
child_terminal_account, product1, val_date
|
|
179
|
+
) == pytest.approx((valid_claim1.shares / product1_price_val_date.outstanding_shares), rel=Decimal(1e-6))
|
|
180
|
+
assert account_manager.get_terminal_account_holding_ratio(
|
|
181
|
+
child_terminal_account, product1, next_val_date
|
|
182
|
+
) == pytest.approx(
|
|
183
|
+
(valid_claim1.shares + valid_claim2.shares) / product1_price_next_val_date.outstanding_shares,
|
|
184
|
+
rel=Decimal(1e-6),
|
|
185
|
+
)
|
|
186
|
+
assert account_manager.get_terminal_account_holding_ratio(
|
|
187
|
+
child_terminal_account, product2, val_date
|
|
188
|
+
) == pytest.approx((valid_claim3.shares / product2_price_val_date.outstanding_shares), rel=Decimal(1e-6))
|
|
189
|
+
assert account_manager.get_terminal_account_holding_ratio(
|
|
190
|
+
child_terminal_account, product2, next_val_date
|
|
191
|
+
) == pytest.approx((valid_claim3.shares / product2_price_next_val_date.outstanding_shares), rel=Decimal(1e-6))
|
|
192
|
+
|
|
193
|
+
@pytest.mark.parametrize("val_date", [fake.date_object()])
|
|
194
|
+
def test_get_root_account_total_holding(
|
|
195
|
+
self, account_factory, claim_factory, product_factory, instrument_price_factory, val_date, commission_type
|
|
196
|
+
):
|
|
197
|
+
product1 = product_factory.create()
|
|
198
|
+
product2 = product_factory.create()
|
|
199
|
+
val_date = (val_date + BDay(0)).date()
|
|
200
|
+
next_val_date = (val_date + BDay(1)).date()
|
|
201
|
+
product1_price_val_date = instrument_price_factory.create(date=val_date, instrument=product1, calculated=False)
|
|
202
|
+
product1_price_next_val_date = instrument_price_factory.create(
|
|
203
|
+
date=next_val_date, instrument=product1, calculated=False
|
|
204
|
+
)
|
|
205
|
+
product2_price_val_date = instrument_price_factory.create(date=val_date, instrument=product2, calculated=False)
|
|
206
|
+
product2_price_next_val_date = instrument_price_factory.create(
|
|
207
|
+
date=next_val_date, instrument=product2, calculated=False
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
parent_account = account_factory.create(is_terminal_account=False, is_active=True)
|
|
211
|
+
claim_factory.create(
|
|
212
|
+
trade__value_date=val_date,
|
|
213
|
+
trade__transaction_date=(val_date - BDay(1)).date(),
|
|
214
|
+
trade__underlying_instrument=product1,
|
|
215
|
+
status=Claim.Status.APPROVED,
|
|
216
|
+
account=parent_account,
|
|
217
|
+
) # ignore this claim because it's attached to a non terminal account
|
|
218
|
+
|
|
219
|
+
child_terminal_account = account_factory.create(
|
|
220
|
+
parent=parent_account, is_terminal_account=True, is_active=True
|
|
221
|
+
)
|
|
222
|
+
claim_factory.create( # ignore this claim because it's not approved
|
|
223
|
+
trade__value_date=val_date,
|
|
224
|
+
trade__transaction_date=(val_date - BDay(1)).date(),
|
|
225
|
+
account=child_terminal_account,
|
|
226
|
+
trade__underlying_instrument=product1,
|
|
227
|
+
status=random.choice([Claim.Status.DRAFT, Claim.Status.WITHDRAWN, Claim.Status.PENDING]),
|
|
228
|
+
)
|
|
229
|
+
unvalid_child_terminal_account = account_factory.create(parent=parent_account, is_active=False)
|
|
230
|
+
claim_factory.create(
|
|
231
|
+
trade__value_date=val_date,
|
|
232
|
+
trade__transaction_date=(val_date - BDay(1)).date(),
|
|
233
|
+
trade__underlying_instrument=product1,
|
|
234
|
+
status=Claim.Status.APPROVED,
|
|
235
|
+
account=unvalid_child_terminal_account,
|
|
236
|
+
) # ignore this claim as the attached account is not active
|
|
237
|
+
|
|
238
|
+
valid_claim1 = claim_factory.create(
|
|
239
|
+
trade__value_date=val_date,
|
|
240
|
+
trade__transaction_date=(val_date - BDay(1)).date(),
|
|
241
|
+
trade__underlying_instrument=product1,
|
|
242
|
+
status=Claim.Status.APPROVED,
|
|
243
|
+
account=child_terminal_account,
|
|
244
|
+
)
|
|
245
|
+
valid_claim2 = claim_factory.create(
|
|
246
|
+
trade__value_date=next_val_date,
|
|
247
|
+
trade__transaction_date=(next_val_date - BDay(1)).date(),
|
|
248
|
+
trade__underlying_instrument=product1,
|
|
249
|
+
status=Claim.Status.APPROVED,
|
|
250
|
+
account=child_terminal_account,
|
|
251
|
+
)
|
|
252
|
+
valid_claim3 = claim_factory.create(
|
|
253
|
+
trade__value_date=val_date,
|
|
254
|
+
trade__transaction_date=(val_date - BDay(1)).date(),
|
|
255
|
+
trade__underlying_instrument=product2,
|
|
256
|
+
status=Claim.Status.APPROVED,
|
|
257
|
+
account=child_terminal_account,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
account_manager = AccountRebateManager(parent_account, commission_type.key)
|
|
261
|
+
account_manager.initialize()
|
|
262
|
+
|
|
263
|
+
valid_claim1_aum = ( # noqa
|
|
264
|
+
valid_claim1.shares
|
|
265
|
+
* product1_price_val_date.net_value
|
|
266
|
+
* product1_price_val_date.currency_fx_rate_to_usd.value
|
|
267
|
+
)
|
|
268
|
+
valid_claim2_aum = ( # noqa
|
|
269
|
+
valid_claim2.shares
|
|
270
|
+
* product1_price_next_val_date.net_value
|
|
271
|
+
* product1_price_next_val_date.currency_fx_rate_to_usd.value
|
|
272
|
+
)
|
|
273
|
+
valid_claim3_aum = ( # noqa
|
|
274
|
+
valid_claim3.shares
|
|
275
|
+
* product2_price_val_date.net_value
|
|
276
|
+
* product2_price_val_date.currency_fx_rate_to_usd.value
|
|
277
|
+
)
|
|
278
|
+
assert account_manager.get_root_account_total_holding(val_date) == (
|
|
279
|
+
pytest.approx(
|
|
280
|
+
(
|
|
281
|
+
valid_claim1.shares * product1_price_val_date._net_value_usd
|
|
282
|
+
+ valid_claim3.shares * product2_price_val_date._net_value_usd
|
|
283
|
+
),
|
|
284
|
+
rel=Decimal(1e-6),
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
assert account_manager.get_root_account_total_holding(next_val_date) == (
|
|
288
|
+
pytest.approx(
|
|
289
|
+
(valid_claim1.shares + valid_claim2.shares) * product1_price_next_val_date._net_value_usd
|
|
290
|
+
+ valid_claim3.shares * product2_price_next_val_date._net_value_usd,
|
|
291
|
+
rel=Decimal(1e-8),
|
|
292
|
+
)
|
|
293
|
+
)
|