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,217 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from datetime import date, timedelta
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import Any, Generator
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from wbcrm.models import Account
|
|
8
|
+
from wbfdm.models.instruments.instrument_prices import InstrumentPrice
|
|
9
|
+
from wbportfolio.models.products import Product
|
|
10
|
+
from wbportfolio.models.transactions import Claim, Fees
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AccountRebateManager:
|
|
14
|
+
FEE_MAP = {"management": ["MANAGEMENT"], "performance": ["PERFORMANCE", "PERFORMANCE_CRYSTALIZED"]}
|
|
15
|
+
|
|
16
|
+
def __init__(self, root_account: Account, commission_type_key: str):
|
|
17
|
+
self.root_account = root_account
|
|
18
|
+
self.commission_type_key = commission_type_key
|
|
19
|
+
self.terminal_accounts = root_account.get_descendants(include_self=True).filter(
|
|
20
|
+
is_terminal_account=True, is_active=True
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def initialize(self):
|
|
24
|
+
"""
|
|
25
|
+
This method aims to initalize the various dataframe used for this Account Rebate Manager
|
|
26
|
+
"""
|
|
27
|
+
account_claims = Claim.get_valid_and_approved_claims(account=self.root_account)
|
|
28
|
+
# Get products that are among the tree accounts claims
|
|
29
|
+
claim_products = account_claims.values("product").distinct("product")
|
|
30
|
+
|
|
31
|
+
# get the fees as a multi-index matrix
|
|
32
|
+
self.df_fees = pd.DataFrame(
|
|
33
|
+
Fees.valid_objects.filter(
|
|
34
|
+
linked_product__in=claim_products,
|
|
35
|
+
transaction_subtype__in=self.FEE_MAP[self.commission_type_key],
|
|
36
|
+
).values("linked_product", "transaction_date", "total_value")
|
|
37
|
+
)
|
|
38
|
+
if not self.df_fees.empty:
|
|
39
|
+
self.df_fees = (
|
|
40
|
+
self.df_fees.rename(columns={"linked_product": "product", "transaction_date": "date"})
|
|
41
|
+
.groupby(["product", "date"])
|
|
42
|
+
.sum()
|
|
43
|
+
.total_value.astype(float)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# get the shares for the terminal accounts as a multi index matrix
|
|
47
|
+
df_shares = pd.DataFrame(account_claims.values("date_considered", "product", "account", "shares")).rename(
|
|
48
|
+
columns={"date_considered": "date"}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
df_net_value_usd = pd.DataFrame(
|
|
52
|
+
InstrumentPrice.objects.annotate_base_data()
|
|
53
|
+
.filter(instrument__in=claim_products, calculated=False)
|
|
54
|
+
.values("net_value_usd", "date", "instrument")
|
|
55
|
+
).rename(columns={"instrument": "product"})
|
|
56
|
+
|
|
57
|
+
if not df_shares.empty:
|
|
58
|
+
if not self.df_fees.empty:
|
|
59
|
+
timeline = pd.date_range(
|
|
60
|
+
self.df_fees.index.get_level_values("date").min() - timedelta(days=1),
|
|
61
|
+
self.df_fees.index.get_level_values("date").max(),
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
timeline = pd.date_range(df_shares["date"].min(), date.today())
|
|
65
|
+
timeline = [ts.date() for ts in timeline] # Don't know how to do any different but look inefficient to me
|
|
66
|
+
self.df_shares = (
|
|
67
|
+
df_shares[["date", "product", "account", "shares"]]
|
|
68
|
+
.groupby(["account", "product", "date"])
|
|
69
|
+
.sum()
|
|
70
|
+
.astype(float)
|
|
71
|
+
)
|
|
72
|
+
self.df_shares = self.df_shares.reindex(
|
|
73
|
+
pd.MultiIndex.from_product(
|
|
74
|
+
[self.df_shares.index.levels[0], self.df_shares.index.levels[1], timeline],
|
|
75
|
+
names=["account", "product", "date"],
|
|
76
|
+
),
|
|
77
|
+
fill_value=0,
|
|
78
|
+
)
|
|
79
|
+
self.df_shares["shares"] = (
|
|
80
|
+
self.df_shares.groupby(level=["account", "product"])["shares"].cumsum().astype(float)
|
|
81
|
+
)
|
|
82
|
+
if not df_net_value_usd.empty:
|
|
83
|
+
df_net_value_usd = df_net_value_usd.set_index(["product", "date"]).sort_index().astype(float)
|
|
84
|
+
self.df_aum = self.df_shares.join(df_net_value_usd, on=["product", "date"])
|
|
85
|
+
self.df_aum["aum"] = self.df_aum["shares"] * self.df_aum["net_value_usd"]
|
|
86
|
+
self.df_aum = self.df_aum.groupby(["account", "product"]).bfill()["aum"]
|
|
87
|
+
self.df_shares = self.df_shares["shares"]
|
|
88
|
+
|
|
89
|
+
def get_iterator(
|
|
90
|
+
self,
|
|
91
|
+
only_content_object_ids: list[int] | None = None,
|
|
92
|
+
start_date: date | None = None,
|
|
93
|
+
terminal_account_filter_dict: dict[str, Any] | None = None,
|
|
94
|
+
**kwargs
|
|
95
|
+
) -> Generator[tuple[Account, Product, date], None, None]:
|
|
96
|
+
"""
|
|
97
|
+
Given the parameters and the instance root account and commission type, yield all valid terminal account, product and date
|
|
98
|
+
where rebate are expected to be computed
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
only_content_object_ids: list of product to consider. Default to empty (i.e. all tree accounts products)
|
|
102
|
+
start_date: If specified, filter iterator to start only at the given date. Default to None.
|
|
103
|
+
terminal_account_filter_dict: Divers query filter paramters to be filter out terminal accounts
|
|
104
|
+
**kwargs: Optional keyword argument
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
yield the valid terminal account, product and date as a tuple
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
terminal_accounts = self.terminal_accounts.all()
|
|
111
|
+
if terminal_account_filter_dict:
|
|
112
|
+
terminal_accounts = terminal_accounts.filter(**terminal_account_filter_dict)
|
|
113
|
+
products_map = {p.id: p for p in Product.objects.all()}
|
|
114
|
+
if (
|
|
115
|
+
hasattr(self, "df_shares")
|
|
116
|
+
and hasattr(self, "df_fees")
|
|
117
|
+
and not self.df_fees.empty
|
|
118
|
+
and not self.df_shares.empty
|
|
119
|
+
):
|
|
120
|
+
for terminal_account in terminal_accounts:
|
|
121
|
+
with suppress(KeyError):
|
|
122
|
+
# we mask the day where total shares are greater than 0
|
|
123
|
+
potential_df = self.df_shares.loc[(terminal_account.id, slice(None), slice(None))]
|
|
124
|
+
potential_df = potential_df[potential_df > 0]
|
|
125
|
+
# we remove days where there isn't any fees
|
|
126
|
+
potential_df = potential_df.mask(self.df_fees == 0, 0)
|
|
127
|
+
potential_df = potential_df[potential_df > 0].reset_index()
|
|
128
|
+
if not potential_df.empty:
|
|
129
|
+
if only_content_object_ids:
|
|
130
|
+
potential_df = potential_df[potential_df["product"].isin(only_content_object_ids)]
|
|
131
|
+
if start_date:
|
|
132
|
+
potential_df = potential_df[potential_df["date"] >= start_date]
|
|
133
|
+
potential_df["terminal_account"] = terminal_account
|
|
134
|
+
potential_df["product"] = potential_df["product"].map(products_map)
|
|
135
|
+
yield from tuple(potential_df[["terminal_account", "product", "date"]].to_records(index=False))
|
|
136
|
+
|
|
137
|
+
def get_commission_pool(self, product: Product, compute_date: date) -> Decimal:
|
|
138
|
+
"""
|
|
139
|
+
Calculate the commission pool for a specific product on a given date.
|
|
140
|
+
|
|
141
|
+
This function calculates the commission pool associated with a specific product
|
|
142
|
+
on a given compute date. The commission pool represents the accumulated fees
|
|
143
|
+
for the product up to the specified date.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
product (Product): The product for which to calculate the commission pool.
|
|
147
|
+
compute_date (date): The date for which the commission pool is to be computed.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Decimal: The commission pool amount for the specified product and date.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
KeyError: If no commission pool data is available for the given product and date,
|
|
154
|
+
a KeyError will be raised, and the function will return Decimal(0).
|
|
155
|
+
"""
|
|
156
|
+
with suppress(KeyError):
|
|
157
|
+
return Decimal(self.df_fees.loc[(product.id, compute_date)])
|
|
158
|
+
return Decimal(0)
|
|
159
|
+
|
|
160
|
+
def get_terminal_account_holding_ratio(
|
|
161
|
+
self, terminal_account: Account, product: Product, compute_date: date
|
|
162
|
+
) -> Decimal:
|
|
163
|
+
"""
|
|
164
|
+
Calculate the ratio of product shares held by a terminal account.
|
|
165
|
+
|
|
166
|
+
This function calculates the ratio of product shares held by a specific terminal account
|
|
167
|
+
on a given compute date, relative to the outstanding shares of the product on the same date.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
terminal_account (Account): The terminal account for which to calculate the holding ratio.
|
|
171
|
+
product (Product): The product for which the holding ratio is calculated.
|
|
172
|
+
compute_date (date): The date for which the holding ratio is computed.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Decimal: The ratio of product shares held by the terminal account on the given date,
|
|
176
|
+
relative to the outstanding shares of the product. The value is capped at 1.0.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
InstrumentPrice.DoesNotExist: If no price data is available for the given product and date,
|
|
180
|
+
an exception will be caught, and the function will return Decimal(0).
|
|
181
|
+
KeyError: If no holding ratio data is available for the given terminal account, product,
|
|
182
|
+
and date, a KeyError will be caught, and the function will return Decimal(0).
|
|
183
|
+
"""
|
|
184
|
+
with suppress(InstrumentPrice.DoesNotExist, KeyError):
|
|
185
|
+
product_shares = max(product.prices.get(date=compute_date, calculated=True).outstanding_shares, Decimal(0))
|
|
186
|
+
account_shares = max(
|
|
187
|
+
Decimal(self.df_shares.loc[(terminal_account.id, product.id, compute_date)]), Decimal(0)
|
|
188
|
+
)
|
|
189
|
+
if product_shares:
|
|
190
|
+
return min(
|
|
191
|
+
account_shares / product_shares,
|
|
192
|
+
Decimal(1.0), # Cannot have a account share greater than the total product shares
|
|
193
|
+
)
|
|
194
|
+
return Decimal(0)
|
|
195
|
+
|
|
196
|
+
def get_root_account_total_holding(self, compute_date: date) -> Decimal:
|
|
197
|
+
"""
|
|
198
|
+
Calculate the total assets under management (AUM) for the root account.
|
|
199
|
+
|
|
200
|
+
This function calculates the total assets under management (AUM) for the root account
|
|
201
|
+
across all terminal accounts on a specific compute date.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
compute_date (date): The date for which the total AUM is calculated.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Decimal: The total assets under management for the root account on the given date.
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
KeyError: If no AUM data is available for any terminal account on the given date,
|
|
211
|
+
a KeyError will be caught, and the function will return Decimal(0).
|
|
212
|
+
"""
|
|
213
|
+
account_aum = Decimal(0)
|
|
214
|
+
for terminal_account in self.terminal_accounts:
|
|
215
|
+
with suppress(KeyError):
|
|
216
|
+
account_aum += Decimal(self.df_aum.loc[(terminal_account.id, slice(None), compute_date)].sum())
|
|
217
|
+
return account_aum
|