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.

Files changed (76) hide show
  1. wbcommission/__init__.py +1 -0
  2. wbcommission/admin/__init__.py +4 -0
  3. wbcommission/admin/accounts.py +22 -0
  4. wbcommission/admin/commission.py +85 -0
  5. wbcommission/admin/rebate.py +7 -0
  6. wbcommission/analytics/__init__.py +0 -0
  7. wbcommission/analytics/marginality.py +181 -0
  8. wbcommission/apps.py +5 -0
  9. wbcommission/dynamic_preferences_registry.py +0 -0
  10. wbcommission/factories/__init__.py +9 -0
  11. wbcommission/factories/commission.py +100 -0
  12. wbcommission/factories/rebate.py +16 -0
  13. wbcommission/filters/__init__.py +7 -0
  14. wbcommission/filters/rebate.py +187 -0
  15. wbcommission/filters/signals.py +44 -0
  16. wbcommission/generators/__init__.py +2 -0
  17. wbcommission/generators/rebate_generator.py +93 -0
  18. wbcommission/migrations/0001_initial.py +299 -0
  19. wbcommission/migrations/0002_commissionrule_remove_accountcustomer_account_and_more.py +395 -0
  20. wbcommission/migrations/0003_alter_commission_account.py +24 -0
  21. wbcommission/migrations/0004_rebate_audit_log.py +19 -0
  22. wbcommission/migrations/0005_alter_rebate_audit_log.py +20 -0
  23. wbcommission/migrations/0006_commissionrule_consider_zero_percent_for_exclusion.py +21 -0
  24. wbcommission/migrations/0007_remove_commission_unique_crm_recipient_account_and_more.py +50 -0
  25. wbcommission/migrations/0008_alter_commission_options_alter_commission_order.py +26 -0
  26. wbcommission/migrations/__init__.py +0 -0
  27. wbcommission/models/__init__.py +9 -0
  28. wbcommission/models/account_service.py +217 -0
  29. wbcommission/models/commission.py +679 -0
  30. wbcommission/models/rebate.py +319 -0
  31. wbcommission/models/signals.py +45 -0
  32. wbcommission/permissions.py +6 -0
  33. wbcommission/reports/__init__.py +0 -0
  34. wbcommission/reports/audit_report.py +51 -0
  35. wbcommission/reports/customer_report.py +299 -0
  36. wbcommission/reports/utils.py +30 -0
  37. wbcommission/serializers/__init__.py +3 -0
  38. wbcommission/serializers/commissions.py +26 -0
  39. wbcommission/serializers/rebate.py +87 -0
  40. wbcommission/serializers/signals.py +27 -0
  41. wbcommission/tests/__init__.py +0 -0
  42. wbcommission/tests/analytics/__init__.py +0 -0
  43. wbcommission/tests/analytics/test_marginality.py +253 -0
  44. wbcommission/tests/conftest.py +89 -0
  45. wbcommission/tests/models/__init__.py +0 -0
  46. wbcommission/tests/models/mixins.py +22 -0
  47. wbcommission/tests/models/test_account_service.py +293 -0
  48. wbcommission/tests/models/test_commission.py +587 -0
  49. wbcommission/tests/models/test_rebate.py +136 -0
  50. wbcommission/tests/signals.py +0 -0
  51. wbcommission/tests/test_permissions.py +66 -0
  52. wbcommission/tests/viewsets/__init__.py +0 -0
  53. wbcommission/tests/viewsets/test_rebate.py +76 -0
  54. wbcommission/urls.py +42 -0
  55. wbcommission/viewsets/__init__.py +7 -0
  56. wbcommission/viewsets/buttons/__init__.py +2 -0
  57. wbcommission/viewsets/buttons/rebate.py +46 -0
  58. wbcommission/viewsets/buttons/signals.py +53 -0
  59. wbcommission/viewsets/commissions.py +21 -0
  60. wbcommission/viewsets/display/__init__.py +5 -0
  61. wbcommission/viewsets/display/commissions.py +21 -0
  62. wbcommission/viewsets/display/rebate.py +117 -0
  63. wbcommission/viewsets/endpoints/__init__.py +4 -0
  64. wbcommission/viewsets/endpoints/commissions.py +0 -0
  65. wbcommission/viewsets/endpoints/rebate.py +21 -0
  66. wbcommission/viewsets/menu/__init__.py +1 -0
  67. wbcommission/viewsets/menu/commissions.py +0 -0
  68. wbcommission/viewsets/menu/rebate.py +13 -0
  69. wbcommission/viewsets/mixins.py +39 -0
  70. wbcommission/viewsets/rebate.py +481 -0
  71. wbcommission/viewsets/titles/__init__.py +1 -0
  72. wbcommission/viewsets/titles/commissions.py +0 -0
  73. wbcommission/viewsets/titles/rebate.py +11 -0
  74. wbcommission-2.2.1.dist-info/METADATA +11 -0
  75. wbcommission-2.2.1.dist-info/RECORD +76 -0
  76. 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