wbportfolio 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 wbportfolio might be problematic. Click here for more details.

Files changed (486) hide show
  1. wbportfolio/__init__.py +1 -0
  2. wbportfolio/admin/__init__.py +12 -0
  3. wbportfolio/admin/asset.py +47 -0
  4. wbportfolio/admin/custodians.py +9 -0
  5. wbportfolio/admin/portfolio.py +127 -0
  6. wbportfolio/admin/portfolio_relationships.py +22 -0
  7. wbportfolio/admin/product_groups.py +42 -0
  8. wbportfolio/admin/products.py +80 -0
  9. wbportfolio/admin/reconciliations.py +14 -0
  10. wbportfolio/admin/registers.py +17 -0
  11. wbportfolio/admin/roles.py +19 -0
  12. wbportfolio/admin/synchronization/__init__.py +2 -0
  13. wbportfolio/admin/synchronization/admin.py +114 -0
  14. wbportfolio/admin/synchronization/portfolio_synchronization.py +18 -0
  15. wbportfolio/admin/synchronization/price_computation.py +21 -0
  16. wbportfolio/admin/transactions/__init__.py +5 -0
  17. wbportfolio/admin/transactions/claim.py +16 -0
  18. wbportfolio/admin/transactions/dividends.py +14 -0
  19. wbportfolio/admin/transactions/fees.py +35 -0
  20. wbportfolio/admin/transactions/trades.py +49 -0
  21. wbportfolio/admin/transactions/transactions.py +37 -0
  22. wbportfolio/analysis/__init__.py +0 -0
  23. wbportfolio/analysis/claims.py +235 -0
  24. wbportfolio/apps.py +5 -0
  25. wbportfolio/contrib/__init__.py +0 -0
  26. wbportfolio/contrib/company_portfolio/__init__.py +0 -0
  27. wbportfolio/contrib/company_portfolio/admin.py +28 -0
  28. wbportfolio/contrib/company_portfolio/apps.py +29 -0
  29. wbportfolio/contrib/company_portfolio/configs/__init__.py +3 -0
  30. wbportfolio/contrib/company_portfolio/configs/display.py +182 -0
  31. wbportfolio/contrib/company_portfolio/configs/endpoints.py +34 -0
  32. wbportfolio/contrib/company_portfolio/configs/previews.py +37 -0
  33. wbportfolio/contrib/company_portfolio/constants.py +1 -0
  34. wbportfolio/contrib/company_portfolio/dynamic_preferences_registry.py +87 -0
  35. wbportfolio/contrib/company_portfolio/factories.py +32 -0
  36. wbportfolio/contrib/company_portfolio/filters.py +127 -0
  37. wbportfolio/contrib/company_portfolio/management.py +19 -0
  38. wbportfolio/contrib/company_portfolio/migrations/0001_initial.py +214 -0
  39. wbportfolio/contrib/company_portfolio/migrations/__init__.py +0 -0
  40. wbportfolio/contrib/company_portfolio/models.py +334 -0
  41. wbportfolio/contrib/company_portfolio/scripts.py +76 -0
  42. wbportfolio/contrib/company_portfolio/serializers.py +303 -0
  43. wbportfolio/contrib/company_portfolio/tasks.py +19 -0
  44. wbportfolio/contrib/company_portfolio/tests/__init__.py +0 -0
  45. wbportfolio/contrib/company_portfolio/tests/conftest.py +161 -0
  46. wbportfolio/contrib/company_portfolio/tests/test_models.py +161 -0
  47. wbportfolio/contrib/company_portfolio/urls.py +29 -0
  48. wbportfolio/contrib/company_portfolio/viewsets.py +195 -0
  49. wbportfolio/defaults/__init__.py +0 -0
  50. wbportfolio/defaults/fees/__init__.py +0 -0
  51. wbportfolio/defaults/fees/default.py +92 -0
  52. wbportfolio/defaults/portfolio/__init__.py +0 -0
  53. wbportfolio/defaults/portfolio/default_rebalancing.py +45 -0
  54. wbportfolio/dynamic_preferences_registry.py +58 -0
  55. wbportfolio/factories/__init__.py +35 -0
  56. wbportfolio/factories/adjustments.py +17 -0
  57. wbportfolio/factories/assets.py +75 -0
  58. wbportfolio/factories/claim.py +39 -0
  59. wbportfolio/factories/custodians.py +11 -0
  60. wbportfolio/factories/dividends.py +14 -0
  61. wbportfolio/factories/fees.py +15 -0
  62. wbportfolio/factories/indexes.py +17 -0
  63. wbportfolio/factories/portfolio_cash_flow.py +20 -0
  64. wbportfolio/factories/portfolio_cash_targets.py +15 -0
  65. wbportfolio/factories/portfolio_swing_pricings.py +15 -0
  66. wbportfolio/factories/portfolios.py +59 -0
  67. wbportfolio/factories/product_groups.py +28 -0
  68. wbportfolio/factories/products.py +56 -0
  69. wbportfolio/factories/pytest_utils.py +121 -0
  70. wbportfolio/factories/reconciliations.py +23 -0
  71. wbportfolio/factories/roles.py +20 -0
  72. wbportfolio/factories/synchronization.py +40 -0
  73. wbportfolio/factories/trades.py +35 -0
  74. wbportfolio/factories/transactions.py +21 -0
  75. wbportfolio/fdm/__init__.py +0 -0
  76. wbportfolio/fdm/tasks.py +12 -0
  77. wbportfolio/filters/__init__.py +32 -0
  78. wbportfolio/filters/assets.py +485 -0
  79. wbportfolio/filters/assets_and_net_new_money_progression.py +42 -0
  80. wbportfolio/filters/custodians.py +10 -0
  81. wbportfolio/filters/esg.py +22 -0
  82. wbportfolio/filters/performances.py +171 -0
  83. wbportfolio/filters/portfolios.py +24 -0
  84. wbportfolio/filters/positions.py +178 -0
  85. wbportfolio/filters/products.py +157 -0
  86. wbportfolio/filters/roles.py +26 -0
  87. wbportfolio/filters/signals.py +92 -0
  88. wbportfolio/filters/transactions/__init__.py +20 -0
  89. wbportfolio/filters/transactions/claim.py +394 -0
  90. wbportfolio/filters/transactions/fees.py +66 -0
  91. wbportfolio/filters/transactions/trades.py +224 -0
  92. wbportfolio/filters/transactions/transactions.py +98 -0
  93. wbportfolio/import_export/__init__.py +0 -0
  94. wbportfolio/import_export/backends/__init__.py +2 -0
  95. wbportfolio/import_export/backends/ubs/__init__.py +3 -0
  96. wbportfolio/import_export/backends/ubs/asset_position.py +45 -0
  97. wbportfolio/import_export/backends/ubs/fees.py +63 -0
  98. wbportfolio/import_export/backends/ubs/instrument_price.py +44 -0
  99. wbportfolio/import_export/backends/ubs/mixin.py +15 -0
  100. wbportfolio/import_export/backends/utils.py +58 -0
  101. wbportfolio/import_export/backends/wbfdm/__init__.py +2 -0
  102. wbportfolio/import_export/backends/wbfdm/adjustment.py +50 -0
  103. wbportfolio/import_export/backends/wbfdm/dividend.py +16 -0
  104. wbportfolio/import_export/backends/wbfdm/mixin.py +15 -0
  105. wbportfolio/import_export/handlers/__init__.py +0 -0
  106. wbportfolio/import_export/handlers/adjustment.py +39 -0
  107. wbportfolio/import_export/handlers/asset_position.py +167 -0
  108. wbportfolio/import_export/handlers/dividend.py +80 -0
  109. wbportfolio/import_export/handlers/fees.py +58 -0
  110. wbportfolio/import_export/handlers/portfolio_cash_flow.py +57 -0
  111. wbportfolio/import_export/handlers/register.py +43 -0
  112. wbportfolio/import_export/handlers/trade.py +191 -0
  113. wbportfolio/import_export/parsers/__init__.py +0 -0
  114. wbportfolio/import_export/parsers/default_mapping.py +30 -0
  115. wbportfolio/import_export/parsers/jpmorgan/__init__.py +0 -0
  116. wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +63 -0
  117. wbportfolio/import_export/parsers/jpmorgan/fees.py +64 -0
  118. wbportfolio/import_export/parsers/jpmorgan/strategy.py +116 -0
  119. wbportfolio/import_export/parsers/jpmorgan/valuation.py +41 -0
  120. wbportfolio/import_export/parsers/leonteq/__init__.py +0 -0
  121. wbportfolio/import_export/parsers/leonteq/customer_trade.py +47 -0
  122. wbportfolio/import_export/parsers/leonteq/equity.py +81 -0
  123. wbportfolio/import_export/parsers/leonteq/fees.py +70 -0
  124. wbportfolio/import_export/parsers/leonteq/trade.py +94 -0
  125. wbportfolio/import_export/parsers/leonteq/valuation.py +39 -0
  126. wbportfolio/import_export/parsers/natixis/__init__.py +0 -0
  127. wbportfolio/import_export/parsers/natixis/customer_trade.py +62 -0
  128. wbportfolio/import_export/parsers/natixis/d1_customer_trade.py +66 -0
  129. wbportfolio/import_export/parsers/natixis/d1_equity.py +80 -0
  130. wbportfolio/import_export/parsers/natixis/d1_fees.py +58 -0
  131. wbportfolio/import_export/parsers/natixis/d1_trade.py +70 -0
  132. wbportfolio/import_export/parsers/natixis/d1_valuation.py +41 -0
  133. wbportfolio/import_export/parsers/natixis/dividend.py +53 -0
  134. wbportfolio/import_export/parsers/natixis/equity.py +60 -0
  135. wbportfolio/import_export/parsers/natixis/fees.py +53 -0
  136. wbportfolio/import_export/parsers/natixis/trade.py +63 -0
  137. wbportfolio/import_export/parsers/natixis/utils.py +76 -0
  138. wbportfolio/import_export/parsers/natixis/valuation.py +46 -0
  139. wbportfolio/import_export/parsers/refinitiv/__init__.py +0 -0
  140. wbportfolio/import_export/parsers/refinitiv/adjustment.py +24 -0
  141. wbportfolio/import_export/parsers/sg_lux/__init__.py +0 -0
  142. wbportfolio/import_export/parsers/sg_lux/custodian_positions.py +70 -0
  143. wbportfolio/import_export/parsers/sg_lux/customer_trade.py +75 -0
  144. wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +140 -0
  145. wbportfolio/import_export/parsers/sg_lux/customer_trade_slk.py +80 -0
  146. wbportfolio/import_export/parsers/sg_lux/customer_trade_without_pw.py +57 -0
  147. wbportfolio/import_export/parsers/sg_lux/equity.py +137 -0
  148. wbportfolio/import_export/parsers/sg_lux/fees.py +56 -0
  149. wbportfolio/import_export/parsers/sg_lux/perf_fees.py +51 -0
  150. wbportfolio/import_export/parsers/sg_lux/portfolio_cash_flow.py +29 -0
  151. wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py +36 -0
  152. wbportfolio/import_export/parsers/sg_lux/registers.py +210 -0
  153. wbportfolio/import_export/parsers/sg_lux/sylk.py +248 -0
  154. wbportfolio/import_export/parsers/sg_lux/utils.py +36 -0
  155. wbportfolio/import_export/parsers/sg_lux/valuation.py +53 -0
  156. wbportfolio/import_export/parsers/societe_generale/__init__.py +0 -0
  157. wbportfolio/import_export/parsers/societe_generale/customer_trade.py +54 -0
  158. wbportfolio/import_export/parsers/societe_generale/strategy.py +94 -0
  159. wbportfolio/import_export/parsers/societe_generale/valuation.py +37 -0
  160. wbportfolio/import_export/parsers/tellco/__init__.py +0 -0
  161. wbportfolio/import_export/parsers/tellco/customer_trade.py +64 -0
  162. wbportfolio/import_export/parsers/tellco/equity.py +86 -0
  163. wbportfolio/import_export/parsers/tellco/valuation.py +52 -0
  164. wbportfolio/import_export/parsers/ubs/__init__.py +0 -0
  165. wbportfolio/import_export/parsers/ubs/api/__init__.py +0 -0
  166. wbportfolio/import_export/parsers/ubs/api/asset_position.py +106 -0
  167. wbportfolio/import_export/parsers/ubs/api/fees.py +31 -0
  168. wbportfolio/import_export/parsers/ubs/api/instrument_price.py +20 -0
  169. wbportfolio/import_export/parsers/ubs/api/utils.py +0 -0
  170. wbportfolio/import_export/parsers/ubs/customer_trade.py +60 -0
  171. wbportfolio/import_export/parsers/ubs/equity.py +97 -0
  172. wbportfolio/import_export/parsers/ubs/historical_customer_trade.py +67 -0
  173. wbportfolio/import_export/parsers/ubs/valuation.py +52 -0
  174. wbportfolio/import_export/parsers/vontobel/__init__.py +0 -0
  175. wbportfolio/import_export/parsers/vontobel/asset_position.py +97 -0
  176. wbportfolio/import_export/parsers/vontobel/customer_trade.py +54 -0
  177. wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +40 -0
  178. wbportfolio/import_export/parsers/vontobel/instrument.py +34 -0
  179. wbportfolio/import_export/parsers/vontobel/management_fees.py +86 -0
  180. wbportfolio/import_export/parsers/vontobel/performance_fees.py +35 -0
  181. wbportfolio/import_export/parsers/vontobel/trade.py +38 -0
  182. wbportfolio/import_export/parsers/vontobel/utils.py +17 -0
  183. wbportfolio/import_export/parsers/vontobel/valuation.py +29 -0
  184. wbportfolio/import_export/resources/__init__.py +0 -0
  185. wbportfolio/import_export/resources/assets.py +68 -0
  186. wbportfolio/import_export/resources/trades.py +41 -0
  187. wbportfolio/import_export/utils.py +42 -0
  188. wbportfolio/metric/__init__.py +0 -0
  189. wbportfolio/metric/backends/__init__.py +2 -0
  190. wbportfolio/metric/backends/base.py +86 -0
  191. wbportfolio/metric/backends/constants.py +222 -0
  192. wbportfolio/metric/backends/portfolio_base.py +255 -0
  193. wbportfolio/metric/backends/portfolio_esg.py +66 -0
  194. wbportfolio/metric/tests/__init__.py +0 -0
  195. wbportfolio/metric/tests/conftest.py +4 -0
  196. wbportfolio/metric/tests/test_portfolio_base.py +135 -0
  197. wbportfolio/metric/tests/test_portfolio_esg.py +69 -0
  198. wbportfolio/migrations/0001_initial_squashed.py +13848 -0
  199. wbportfolio/migrations/0002_product_default_sub_account_squashed_0039_alter_assetallocation_company_and_more.py +3836 -0
  200. wbportfolio/migrations/0040_instrument_financial_instrument.py +26 -0
  201. wbportfolio/migrations/0041_remove_listresearch_research_ptr_and_more.py +129 -0
  202. wbportfolio/migrations/0042_instrumentlist_instrumentlistthroughmodel_and_more.py +71 -0
  203. wbportfolio/migrations/0043_alter_instrumentlistthroughmodel_options_and_more.py +238 -0
  204. wbportfolio/migrations/0044_alter_instrumentlist_identifier.py +35 -0
  205. wbportfolio/migrations/0045_alter_instrument_financial_instrument.py +26 -0
  206. wbportfolio/migrations/0046_add_product_default_account.py +166 -0
  207. wbportfolio/migrations/0047_remove_product_default_sub_account.py +14 -0
  208. wbportfolio/migrations/0048_alter_trade_status.py +29 -0
  209. wbportfolio/migrations/0049_trade_claimed_shares.py +25 -0
  210. wbportfolio/migrations/0050_fees_fee_date_fees_wbportfolio_transac_1f7a29_idx.py +44 -0
  211. wbportfolio/migrations/0051_delete_macroreview.py +11 -0
  212. wbportfolio/migrations/0052_remove_cash_instrument_ptr_and_more.py +888 -0
  213. wbportfolio/migrations/0053_remove_product_group.py +132 -0
  214. wbportfolio/migrations/0054_portfolioinstrumentpreferredclassificationthroughmodel_and_more.py +270 -0
  215. wbportfolio/migrations/0055_remove_product__custom_management_rebates_and_more.py +139 -0
  216. wbportfolio/migrations/0056_remove_companyportfoliodata_assets_under_management_currency_and_more.py +56 -0
  217. wbportfolio/migrations/0057_alter_portfolio_preferred_instrument_classifications_and_more.py +36 -0
  218. wbportfolio/migrations/0058_pmsinstrument.py +23 -0
  219. wbportfolio/migrations/0059_fees_unique_fees.py +51 -0
  220. wbportfolio/migrations/0060_alter_portfolioportfoliothroughmodel_type.py +21 -0
  221. wbportfolio/migrations/0061_portfolio_bank_accounts_product_bank_account_and_more.py +175 -0
  222. wbportfolio/migrations/0062_alter_dailyportfoliocashflow_options.py +20 -0
  223. wbportfolio/migrations/0063_accountreconciliation_accountreconciliationline_and_more.py +133 -0
  224. wbportfolio/migrations/0064_alter_portfolio_managers_portfolio_is_tracked_and_more.py +40 -0
  225. wbportfolio/migrations/0065_alter_portfolio_managers_claim_as_shares_and_more.py +73 -0
  226. wbportfolio/migrations/0066_assetposition_initial_shares_at_custodian_and_more.py +108 -0
  227. wbportfolio/migrations/0067_assetposition_unique_asset_position.py +77 -0
  228. wbportfolio/migrations/0068_trade_internal_trade_trade_marked_as_internal_and_more.py +59 -0
  229. wbportfolio/migrations/0069_remove_portfolio_is_invested_and_more.py +56 -0
  230. wbportfolio/migrations/0070_remove_assetposition_unique_asset_position_and_more.py +82 -0
  231. wbportfolio/migrations/0071_alter_trade_options_alter_trade_order.py +22 -0
  232. wbportfolio/migrations/__init__.py +0 -0
  233. wbportfolio/models/__init__.py +26 -0
  234. wbportfolio/models/adjustments.py +246 -0
  235. wbportfolio/models/asset.py +869 -0
  236. wbportfolio/models/custodians.py +101 -0
  237. wbportfolio/models/indexes.py +33 -0
  238. wbportfolio/models/mixins/__init__.py +0 -0
  239. wbportfolio/models/mixins/instruments.py +127 -0
  240. wbportfolio/models/mixins/liquidity_stress_test.py +1307 -0
  241. wbportfolio/models/portfolio.py +1039 -0
  242. wbportfolio/models/portfolio_cash_flow.py +167 -0
  243. wbportfolio/models/portfolio_cash_targets.py +46 -0
  244. wbportfolio/models/portfolio_relationship.py +135 -0
  245. wbportfolio/models/portfolio_swing_pricings.py +51 -0
  246. wbportfolio/models/product_groups.py +230 -0
  247. wbportfolio/models/products.py +569 -0
  248. wbportfolio/models/reconciliations/__init__.py +2 -0
  249. wbportfolio/models/reconciliations/account_reconciliation_lines.py +192 -0
  250. wbportfolio/models/reconciliations/account_reconciliations.py +102 -0
  251. wbportfolio/models/reconciliations/reconciliations.py +25 -0
  252. wbportfolio/models/registers.py +132 -0
  253. wbportfolio/models/roles.py +208 -0
  254. wbportfolio/models/synchronization/__init__.py +3 -0
  255. wbportfolio/models/synchronization/portfolio_synchronization.py +292 -0
  256. wbportfolio/models/synchronization/price_computation.py +200 -0
  257. wbportfolio/models/synchronization/synchronization.py +188 -0
  258. wbportfolio/models/transactions/__init__.py +7 -0
  259. wbportfolio/models/transactions/claim.py +634 -0
  260. wbportfolio/models/transactions/dividends.py +31 -0
  261. wbportfolio/models/transactions/expiry.py +7 -0
  262. wbportfolio/models/transactions/fees.py +153 -0
  263. wbportfolio/models/transactions/trade_proposals.py +502 -0
  264. wbportfolio/models/transactions/trades.py +704 -0
  265. wbportfolio/models/transactions/transactions.py +211 -0
  266. wbportfolio/models/utils.py +12 -0
  267. wbportfolio/permissions.py +13 -0
  268. wbportfolio/pms/__init__.py +0 -0
  269. wbportfolio/pms/statistics/__init__.py +0 -0
  270. wbportfolio/pms/trading/__init__.py +1 -0
  271. wbportfolio/pms/trading/handler.py +164 -0
  272. wbportfolio/pms/typing.py +194 -0
  273. wbportfolio/preferences.py +6 -0
  274. wbportfolio/reports/__init__.py +0 -0
  275. wbportfolio/reports/monthly_position_report.py +74 -0
  276. wbportfolio/risk_management/__init__.py +0 -0
  277. wbportfolio/risk_management/backends/__init__.py +11 -0
  278. wbportfolio/risk_management/backends/accounts.py +166 -0
  279. wbportfolio/risk_management/backends/controversy_portfolio.py +63 -0
  280. wbportfolio/risk_management/backends/exposure_portfolio.py +203 -0
  281. wbportfolio/risk_management/backends/instrument_list_portfolio.py +89 -0
  282. wbportfolio/risk_management/backends/liquidity_risk.py +86 -0
  283. wbportfolio/risk_management/backends/liquidity_stress_instrument.py +86 -0
  284. wbportfolio/risk_management/backends/mixins.py +220 -0
  285. wbportfolio/risk_management/backends/product_integrity.py +111 -0
  286. wbportfolio/risk_management/backends/stop_loss_instrument.py +24 -0
  287. wbportfolio/risk_management/backends/stop_loss_portfolio.py +36 -0
  288. wbportfolio/risk_management/backends/ucits_portfolio.py +63 -0
  289. wbportfolio/risk_management/tests/__init__.py +0 -0
  290. wbportfolio/risk_management/tests/conftest.py +15 -0
  291. wbportfolio/risk_management/tests/test_accounts.py +98 -0
  292. wbportfolio/risk_management/tests/test_controversy_portfolio.py +33 -0
  293. wbportfolio/risk_management/tests/test_exposure_portfolio.py +94 -0
  294. wbportfolio/risk_management/tests/test_instrument_list_portfolio.py +60 -0
  295. wbportfolio/risk_management/tests/test_liquidity_risk.py +47 -0
  296. wbportfolio/risk_management/tests/test_product_integrity.py +55 -0
  297. wbportfolio/risk_management/tests/test_stop_loss_instrument.py +110 -0
  298. wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +119 -0
  299. wbportfolio/risk_management/tests/test_ucits_portfolio.py +39 -0
  300. wbportfolio/serializers/__init__.py +42 -0
  301. wbportfolio/serializers/adjustments.py +24 -0
  302. wbportfolio/serializers/assets.py +166 -0
  303. wbportfolio/serializers/custodians.py +26 -0
  304. wbportfolio/serializers/portfolio_cash_flow.py +48 -0
  305. wbportfolio/serializers/portfolio_cash_targets.py +20 -0
  306. wbportfolio/serializers/portfolio_relationship.py +53 -0
  307. wbportfolio/serializers/portfolio_swing_pricing.py +20 -0
  308. wbportfolio/serializers/portfolios.py +143 -0
  309. wbportfolio/serializers/positions.py +76 -0
  310. wbportfolio/serializers/product_group.py +88 -0
  311. wbportfolio/serializers/products.py +331 -0
  312. wbportfolio/serializers/reconciliations.py +171 -0
  313. wbportfolio/serializers/registers.py +72 -0
  314. wbportfolio/serializers/roles.py +60 -0
  315. wbportfolio/serializers/signals.py +157 -0
  316. wbportfolio/serializers/synchronization.py +18 -0
  317. wbportfolio/serializers/transactions/__init__.py +24 -0
  318. wbportfolio/serializers/transactions/claim.py +310 -0
  319. wbportfolio/serializers/transactions/dividends.py +18 -0
  320. wbportfolio/serializers/transactions/expiry.py +18 -0
  321. wbportfolio/serializers/transactions/fees.py +32 -0
  322. wbportfolio/serializers/transactions/trades.py +315 -0
  323. wbportfolio/serializers/transactions/transactions.py +84 -0
  324. wbportfolio/tasks.py +125 -0
  325. wbportfolio/tests/__init__.py +0 -0
  326. wbportfolio/tests/conftest.py +164 -0
  327. wbportfolio/tests/models/__init__.py +0 -0
  328. wbportfolio/tests/models/test_account_reconciliation.py +191 -0
  329. wbportfolio/tests/models/test_assets.py +193 -0
  330. wbportfolio/tests/models/test_custodians.py +12 -0
  331. wbportfolio/tests/models/test_customer_trades.py +113 -0
  332. wbportfolio/tests/models/test_dividends.py +7 -0
  333. wbportfolio/tests/models/test_imports.py +192 -0
  334. wbportfolio/tests/models/test_instrument_mixins.py +48 -0
  335. wbportfolio/tests/models/test_merge.py +133 -0
  336. wbportfolio/tests/models/test_portfolio_cash_flow.py +112 -0
  337. wbportfolio/tests/models/test_portfolio_cash_targets.py +27 -0
  338. wbportfolio/tests/models/test_portfolio_swing_pricings.py +42 -0
  339. wbportfolio/tests/models/test_portfolios.py +676 -0
  340. wbportfolio/tests/models/test_product_groups.py +80 -0
  341. wbportfolio/tests/models/test_products.py +187 -0
  342. wbportfolio/tests/models/test_roles.py +82 -0
  343. wbportfolio/tests/models/test_splits.py +233 -0
  344. wbportfolio/tests/models/test_synchronization.py +617 -0
  345. wbportfolio/tests/models/transactions/__init__.py +0 -0
  346. wbportfolio/tests/models/transactions/test_claim.py +129 -0
  347. wbportfolio/tests/models/transactions/test_fees.py +65 -0
  348. wbportfolio/tests/models/transactions/test_trades.py +204 -0
  349. wbportfolio/tests/models/utils.py +13 -0
  350. wbportfolio/tests/serializers/__init__.py +0 -0
  351. wbportfolio/tests/serializers/test_claims.py +21 -0
  352. wbportfolio/tests/signals.py +151 -0
  353. wbportfolio/tests/tests.py +31 -0
  354. wbportfolio/tests/viewsets/__init__.py +0 -0
  355. wbportfolio/tests/viewsets/test_assets.py +67 -0
  356. wbportfolio/tests/viewsets/test_performances.py +72 -0
  357. wbportfolio/tests/viewsets/test_products.py +92 -0
  358. wbportfolio/tests/viewsets/transactions/__init__.py +0 -0
  359. wbportfolio/tests/viewsets/transactions/test_claims.py +146 -0
  360. wbportfolio/urls.py +247 -0
  361. wbportfolio/utils.py +30 -0
  362. wbportfolio/viewsets/__init__.py +57 -0
  363. wbportfolio/viewsets/adjustments.py +46 -0
  364. wbportfolio/viewsets/assets.py +562 -0
  365. wbportfolio/viewsets/assets_and_net_new_money_progression.py +117 -0
  366. wbportfolio/viewsets/charts/__init__.py +1 -0
  367. wbportfolio/viewsets/charts/assets.py +247 -0
  368. wbportfolio/viewsets/configs/__init__.py +6 -0
  369. wbportfolio/viewsets/configs/buttons/__init__.py +23 -0
  370. wbportfolio/viewsets/configs/buttons/adjustments.py +13 -0
  371. wbportfolio/viewsets/configs/buttons/assets.py +145 -0
  372. wbportfolio/viewsets/configs/buttons/claims.py +83 -0
  373. wbportfolio/viewsets/configs/buttons/custodians.py +76 -0
  374. wbportfolio/viewsets/configs/buttons/fees.py +14 -0
  375. wbportfolio/viewsets/configs/buttons/mixins.py +88 -0
  376. wbportfolio/viewsets/configs/buttons/portfolios.py +115 -0
  377. wbportfolio/viewsets/configs/buttons/products.py +41 -0
  378. wbportfolio/viewsets/configs/buttons/reconciliations.py +65 -0
  379. wbportfolio/viewsets/configs/buttons/registers.py +11 -0
  380. wbportfolio/viewsets/configs/buttons/signals.py +68 -0
  381. wbportfolio/viewsets/configs/buttons/trade_proposals.py +25 -0
  382. wbportfolio/viewsets/configs/buttons/trades.py +144 -0
  383. wbportfolio/viewsets/configs/display/__init__.py +61 -0
  384. wbportfolio/viewsets/configs/display/adjustments.py +81 -0
  385. wbportfolio/viewsets/configs/display/assets.py +265 -0
  386. wbportfolio/viewsets/configs/display/claim.py +299 -0
  387. wbportfolio/viewsets/configs/display/custodians.py +24 -0
  388. wbportfolio/viewsets/configs/display/esg.py +88 -0
  389. wbportfolio/viewsets/configs/display/fees.py +133 -0
  390. wbportfolio/viewsets/configs/display/portfolio_cash_flow.py +103 -0
  391. wbportfolio/viewsets/configs/display/portfolio_relationship.py +38 -0
  392. wbportfolio/viewsets/configs/display/portfolios.py +125 -0
  393. wbportfolio/viewsets/configs/display/positions.py +75 -0
  394. wbportfolio/viewsets/configs/display/product_groups.py +54 -0
  395. wbportfolio/viewsets/configs/display/product_performance.py +241 -0
  396. wbportfolio/viewsets/configs/display/products.py +249 -0
  397. wbportfolio/viewsets/configs/display/reconciliations.py +151 -0
  398. wbportfolio/viewsets/configs/display/registers.py +71 -0
  399. wbportfolio/viewsets/configs/display/roles.py +49 -0
  400. wbportfolio/viewsets/configs/display/trade_proposals.py +97 -0
  401. wbportfolio/viewsets/configs/display/trades.py +359 -0
  402. wbportfolio/viewsets/configs/display/transactions.py +55 -0
  403. wbportfolio/viewsets/configs/endpoints/__init__.py +75 -0
  404. wbportfolio/viewsets/configs/endpoints/adjustments.py +17 -0
  405. wbportfolio/viewsets/configs/endpoints/assets.py +115 -0
  406. wbportfolio/viewsets/configs/endpoints/claim.py +106 -0
  407. wbportfolio/viewsets/configs/endpoints/custodians.py +6 -0
  408. wbportfolio/viewsets/configs/endpoints/esg.py +14 -0
  409. wbportfolio/viewsets/configs/endpoints/fees.py +26 -0
  410. wbportfolio/viewsets/configs/endpoints/portfolio_relationship.py +23 -0
  411. wbportfolio/viewsets/configs/endpoints/portfolios.py +43 -0
  412. wbportfolio/viewsets/configs/endpoints/positions.py +18 -0
  413. wbportfolio/viewsets/configs/endpoints/product_groups.py +11 -0
  414. wbportfolio/viewsets/configs/endpoints/product_performance.py +29 -0
  415. wbportfolio/viewsets/configs/endpoints/products.py +37 -0
  416. wbportfolio/viewsets/configs/endpoints/reconciliations.py +31 -0
  417. wbportfolio/viewsets/configs/endpoints/roles.py +9 -0
  418. wbportfolio/viewsets/configs/endpoints/trade_proposals.py +17 -0
  419. wbportfolio/viewsets/configs/endpoints/trades.py +82 -0
  420. wbportfolio/viewsets/configs/endpoints/transactions.py +17 -0
  421. wbportfolio/viewsets/configs/menu/__init__.py +30 -0
  422. wbportfolio/viewsets/configs/menu/adjustments.py +8 -0
  423. wbportfolio/viewsets/configs/menu/assets.py +8 -0
  424. wbportfolio/viewsets/configs/menu/claim.py +41 -0
  425. wbportfolio/viewsets/configs/menu/custodians.py +11 -0
  426. wbportfolio/viewsets/configs/menu/fees.py +13 -0
  427. wbportfolio/viewsets/configs/menu/instrument_prices.py +10 -0
  428. wbportfolio/viewsets/configs/menu/portfolio_cash_flow.py +8 -0
  429. wbportfolio/viewsets/configs/menu/portfolios.py +15 -0
  430. wbportfolio/viewsets/configs/menu/positions.py +14 -0
  431. wbportfolio/viewsets/configs/menu/product_groups.py +10 -0
  432. wbportfolio/viewsets/configs/menu/product_performance.py +25 -0
  433. wbportfolio/viewsets/configs/menu/products.py +15 -0
  434. wbportfolio/viewsets/configs/menu/reconciliations.py +7 -0
  435. wbportfolio/viewsets/configs/menu/registers.py +10 -0
  436. wbportfolio/viewsets/configs/menu/roles.py +16 -0
  437. wbportfolio/viewsets/configs/menu/trades.py +18 -0
  438. wbportfolio/viewsets/configs/menu/transactions.py +8 -0
  439. wbportfolio/viewsets/configs/previews/__init__.py +1 -0
  440. wbportfolio/viewsets/configs/previews/portfolios.py +21 -0
  441. wbportfolio/viewsets/configs/titles/__init__.py +65 -0
  442. wbportfolio/viewsets/configs/titles/adjustments.py +19 -0
  443. wbportfolio/viewsets/configs/titles/assets.py +57 -0
  444. wbportfolio/viewsets/configs/titles/assets_and_net_new_money_progression.py +6 -0
  445. wbportfolio/viewsets/configs/titles/claim.py +81 -0
  446. wbportfolio/viewsets/configs/titles/custodians.py +12 -0
  447. wbportfolio/viewsets/configs/titles/esg.py +10 -0
  448. wbportfolio/viewsets/configs/titles/fees.py +25 -0
  449. wbportfolio/viewsets/configs/titles/instrument_prices.py +20 -0
  450. wbportfolio/viewsets/configs/titles/portfolios.py +32 -0
  451. wbportfolio/viewsets/configs/titles/positions.py +11 -0
  452. wbportfolio/viewsets/configs/titles/product_groups.py +12 -0
  453. wbportfolio/viewsets/configs/titles/product_performance.py +16 -0
  454. wbportfolio/viewsets/configs/titles/products.py +6 -0
  455. wbportfolio/viewsets/configs/titles/registers.py +12 -0
  456. wbportfolio/viewsets/configs/titles/roles.py +23 -0
  457. wbportfolio/viewsets/configs/titles/trades.py +51 -0
  458. wbportfolio/viewsets/configs/titles/transactions.py +8 -0
  459. wbportfolio/viewsets/custodians.py +66 -0
  460. wbportfolio/viewsets/esg.py +165 -0
  461. wbportfolio/viewsets/mixins.py +48 -0
  462. wbportfolio/viewsets/portfolio_cash_flow.py +31 -0
  463. wbportfolio/viewsets/portfolio_cash_targets.py +8 -0
  464. wbportfolio/viewsets/portfolio_relationship.py +46 -0
  465. wbportfolio/viewsets/portfolio_swing_pricing.py +8 -0
  466. wbportfolio/viewsets/portfolios.py +154 -0
  467. wbportfolio/viewsets/positions.py +292 -0
  468. wbportfolio/viewsets/product_groups.py +84 -0
  469. wbportfolio/viewsets/product_performance.py +646 -0
  470. wbportfolio/viewsets/products.py +529 -0
  471. wbportfolio/viewsets/reconciliations.py +160 -0
  472. wbportfolio/viewsets/registers.py +75 -0
  473. wbportfolio/viewsets/roles.py +44 -0
  474. wbportfolio/viewsets/signals.py +42 -0
  475. wbportfolio/viewsets/synchronization.py +25 -0
  476. wbportfolio/viewsets/transactions/__init__.py +40 -0
  477. wbportfolio/viewsets/transactions/claim.py +933 -0
  478. wbportfolio/viewsets/transactions/fees.py +190 -0
  479. wbportfolio/viewsets/transactions/mixins.py +19 -0
  480. wbportfolio/viewsets/transactions/trade_proposals.py +93 -0
  481. wbportfolio/viewsets/transactions/trades.py +395 -0
  482. wbportfolio/viewsets/transactions/transactions.py +123 -0
  483. wbportfolio-2.2.1.dist-info/METADATA +21 -0
  484. wbportfolio-2.2.1.dist-info/RECORD +486 -0
  485. wbportfolio-2.2.1.dist-info/WHEEL +5 -0
  486. wbportfolio-2.2.1.dist-info/licenses/LICENSE +4 -0
@@ -0,0 +1,11 @@
1
+ from .accounts import *
2
+ from .controversy_portfolio import *
3
+ from .exposure_portfolio import *
4
+ from .instrument_list_portfolio import *
5
+ from .liquidity_stress_instrument import *
6
+ from .stop_loss_instrument import *
7
+ from .stop_loss_portfolio import *
8
+ from .ucits_portfolio import *
9
+ from .accounts import *
10
+ from .product_integrity import *
11
+ from .liquidity_risk import *
@@ -0,0 +1,166 @@
1
+ from datetime import timedelta
2
+ from typing import Generator
3
+
4
+ import pandas as pd
5
+ from django.contrib.contenttypes.models import ContentType
6
+ from django.db import models
7
+ from wbcompliance.models.risk_management import backend
8
+ from wbcompliance.models.risk_management.dispatch import register
9
+ from wbcore import serializers as wb_serializers
10
+ from wbcore.contrib.directory.models import Entry
11
+ from wbcrm.models import Account
12
+ from wbfdm.models import Classification
13
+ from wbfdm.preferences import get_default_classification_group
14
+ from wbportfolio.analysis.claims import ConsolidatedTradeSummary
15
+ from wbportfolio.models import Product, ProductGroup
16
+ from wbportfolio.models.transactions.claim import Claim, ClaimGroupbyChoice
17
+ from wbportfolio.serializers import ProductRepresentationSerializer
18
+
19
+
20
+ @register("Account Shares Rule Backend", rule_group_key="sales")
21
+ class RuleBackend(backend.AbstractRuleBackend):
22
+ OBJECT_FIELD_NAME: str = "customer"
23
+
24
+ customer: Entry
25
+
26
+ class FieldChoices(models.TextChoices):
27
+ SHARES = "SHARES", "Shares"
28
+ AUM = "AUM", "AUM"
29
+
30
+ def __init__(self, *args, **kwargs) -> None:
31
+ super().__init__(*args, **kwargs)
32
+ self.end_date = self.evaluation_date
33
+ self.start_date = (self.evaluation_date - pd.tseries.offsets.BDay(self.business_days_interval)).date()
34
+
35
+ self.group_by = ClaimGroupbyChoice[self.group_by]
36
+ self.field = self.FieldChoices[self.field]
37
+
38
+ def is_passive_evaluation_valid(self) -> bool:
39
+ return Claim.objects.filter_for_customer(self.customer).filter(date__lte=self.evaluation_date).exists()
40
+
41
+ @classmethod
42
+ def get_allowed_content_type(cls) -> "ContentType":
43
+ return ContentType.objects.get_for_model(Entry)
44
+
45
+ def _build_dto_args(self):
46
+ qs = Claim.objects.filter_for_customer(self.customer).filter(
47
+ status=Claim.Status.APPROVED, date__lte=self.evaluation_date
48
+ )
49
+ if self.only_products:
50
+ qs = qs.filter(product__in=self.only_products)
51
+ groupby_map = ClaimGroupbyChoice.get_map(self.group_by.name)
52
+ pivot = groupby_map["pk"]
53
+ pivot_label = groupby_map["title_key"]
54
+ cts_generator = ConsolidatedTradeSummary(
55
+ qs,
56
+ self.start_date,
57
+ self.end_date + timedelta(days=1), # we shift by one because end date is excluded
58
+ pivot,
59
+ pivot_label,
60
+ classification_group=get_default_classification_group(),
61
+ )
62
+ return (cts_generator,)
63
+
64
+ @classmethod
65
+ def get_all_active_relationships(cls) -> models.QuerySet:
66
+ return Entry.objects.annotate(
67
+ has_open_account=models.Exists(Account.open_objects.filter(owner=models.OuterRef("pk")))
68
+ ).filter(has_open_account=True)
69
+
70
+ @classmethod
71
+ def get_serializer_class(cls) -> wb_serializers.Serializer:
72
+ class RuleBackendSerializer(wb_serializers.Serializer):
73
+ business_days_interval = wb_serializers.IntegerField(default=7)
74
+ moving_average_window = wb_serializers.IntegerField(default=1) # 1 means the initial time series
75
+
76
+ only_products = wb_serializers.PrimaryKeyRelatedField(
77
+ queryset=Product.objects.all(),
78
+ many=True,
79
+ default=None,
80
+ allow_null=True,
81
+ label="Only Products",
82
+ )
83
+ _only_products = ProductRepresentationSerializer(many=True, source="parameters__only_products")
84
+
85
+ group_by = wb_serializers.ChoiceField(
86
+ choices=ClaimGroupbyChoice.choices(),
87
+ default=ClaimGroupbyChoice.ACCOUNT,
88
+ allow_null=True,
89
+ help_text="Choose how to group by shares",
90
+ label="Group By",
91
+ )
92
+ field = wb_serializers.ChoiceField(
93
+ choices=cls.FieldChoices.choices,
94
+ default=cls.FieldChoices.SHARES,
95
+ allow_null=True,
96
+ help_text="Choose which metric to choose",
97
+ label="Field",
98
+ )
99
+
100
+ @classmethod
101
+ def get_parameter_fields(cls):
102
+ return [
103
+ "field",
104
+ "group_by",
105
+ "business_days_interval",
106
+ "moving_average_window",
107
+ "only_products",
108
+ ]
109
+
110
+ return RuleBackendSerializer
111
+
112
+ def _process_dto(
113
+ self, cts_generator: ConsolidatedTradeSummary, **kwargs
114
+ ) -> Generator[backend.IncidentResult, None, None]:
115
+ df = cts_generator.get_aum_df()
116
+ if df.empty:
117
+ return
118
+ if self.field == self.FieldChoices.SHARES:
119
+ perf = df["sum_shares_perf"]
120
+ start_df = df["sum_shares_start"]
121
+ end_df = df["sum_shares_end"]
122
+ else:
123
+ perf = df["sum_aum_perf"]
124
+ start_df = df["sum_aum_start"]
125
+ end_df = df["sum_aum_end"]
126
+ perf = perf.dropna()
127
+ if not perf.empty:
128
+ for threshold in self.thresholds:
129
+ numerical_range = threshold.numerical_range
130
+ breached_perf = perf[(perf > numerical_range[0]) & (perf < numerical_range[1])].dropna()
131
+ if not breached_perf.empty:
132
+ for breached_obj_id, percentage in breached_perf.to_dict().items():
133
+ breached_obj = None
134
+ if self.group_by == ClaimGroupbyChoice.PRODUCT:
135
+ breached_obj = Product.objects.get(id=breached_obj_id)
136
+ elif self.group_by == ClaimGroupbyChoice.PRODUCT_GROUP:
137
+ breached_obj = ProductGroup.objects.get(id=breached_obj_id)
138
+ elif self.group_by == ClaimGroupbyChoice.CLASSIFICATION:
139
+ breached_obj = Classification.objects.get(id=breached_obj_id)
140
+ elif self.group_by in [ClaimGroupbyChoice.ACCOUNT, ClaimGroupbyChoice.ROOT_ACCOUNT]:
141
+ breached_obj = Account.objects.get(id=breached_obj_id)
142
+ elif self.group_by in [
143
+ ClaimGroupbyChoice.ACCOUNT_OWNER,
144
+ ClaimGroupbyChoice.ROOT_ACCOUNT_OWNER,
145
+ ]:
146
+ breached_obj = Entry.objects.get(id=breached_obj_id)
147
+ report_details = {
148
+ "Period": f"{cts_generator.start_date:%d.%m.%Y} - {cts_generator.end_date:%d.%m.%Y}",
149
+ }
150
+ color = "red" if percentage < 0 else "green"
151
+ if self.field == "AUM":
152
+ report_details[
153
+ "AUM Change"
154
+ ] = f'<span style="color:{color}">{start_df.loc[breached_obj_id]:.0f} $ → {end_df.loc[breached_obj_id]:.0f} $</span>'
155
+ else:
156
+ report_details[
157
+ "Shares Change"
158
+ ] = f"<span style='color:{color}'>{start_df.loc[breached_obj_id]:.0f} → {end_df.loc[breached_obj_id]:.0f}</span>"
159
+ report_details["Group By"] = self.group_by.value
160
+ yield backend.IncidentResult(
161
+ breached_object=breached_obj,
162
+ breached_object_repr=str(breached_obj),
163
+ breached_value=f'<span style="color:{color}">{percentage:+,.2%}</span>',
164
+ report_details=report_details,
165
+ severity=threshold.severity,
166
+ )
@@ -0,0 +1,63 @@
1
+ from typing import Generator
2
+
3
+ from wbcompliance.models.risk_management import backend
4
+ from wbcompliance.models.risk_management.dispatch import register
5
+ from wbcompliance.models.risk_management.rules import RiskIncidentType
6
+ from wbcore import serializers as wb_serializers
7
+ from wbfdm.enums import ESGControveryFlag
8
+ from wbfdm.models import Instrument
9
+ from wbfdm.models.esg.controversies import Controversy
10
+ from wbportfolio.pms.typing import Portfolio as PortfolioDTO
11
+
12
+ from .mixins import ActivePortfolioRelationshipMixin
13
+
14
+
15
+ @register("Controversy Portfolio Rule Backend", rule_group_key="portfolio")
16
+ class RuleBackend(ActivePortfolioRelationshipMixin):
17
+ def __init__(self, *args, **kwargs):
18
+ super().__init__(*args, **kwargs)
19
+
20
+ # if thresholds are attached to this rule, we take the first as severity. Otherwise, we get the risk incident with the highest severity (e.g. Critical)
21
+ if self.thresholds:
22
+ self.severity = self.thresholds[0].severity
23
+ else:
24
+ self.severity = RiskIncidentType.objects.order_by("-severity_order").first()
25
+ self.flags_repr = [ESGControveryFlag[f].label for f in self.flags]
26
+
27
+ @classmethod
28
+ def get_serializer_class(cls) -> wb_serializers.Serializer:
29
+ class RuleBackendSerializer(wb_serializers.Serializer):
30
+ flags = wb_serializers.MultipleChoiceField(
31
+ choices=ESGControveryFlag.choices,
32
+ default=[ESGControveryFlag.ORANGE.value, ESGControveryFlag.RED.value],
33
+ label="Flags",
34
+ help_text="Set the flags that will trigger the rule",
35
+ )
36
+
37
+ @classmethod
38
+ def get_parameter_fields(cls):
39
+ return [
40
+ "flags",
41
+ ]
42
+
43
+ return RuleBackendSerializer
44
+
45
+ def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
46
+ for instrument_id, weight in portfolio.positions_map.items():
47
+ instrument = Instrument.objects.get(id=instrument_id)
48
+ if (
49
+ controversies := Controversy.objects.filter(
50
+ instrument__in=instrument.get_ancestors(include_self=True), flag__in=self.flags
51
+ )
52
+ ).exists():
53
+ controversies_headlines = "".join([f"<li>{c.headline}</li>" for c in controversies])
54
+ yield backend.IncidentResult(
55
+ breached_object=instrument,
56
+ breached_object_repr=str(instrument),
57
+ breached_value=f"# {controversies.count()}",
58
+ report_details={
59
+ "Controversies Flags": ", ".join(self.flags_repr),
60
+ "Controversies Headlines": controversies_headlines,
61
+ },
62
+ severity=self.severity,
63
+ )
@@ -0,0 +1,203 @@
1
+ from typing import Generator
2
+
3
+ from django.db import models
4
+ from wbcompliance.models.risk_management import backend
5
+ from wbcompliance.models.risk_management.dispatch import register
6
+ from wbcompliance.models.risk_management.rules import RiskIncidentType
7
+ from wbcore import serializers as wb_serializers
8
+ from wbcore.contrib.currency.models import Currency
9
+ from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
10
+ from wbcore.contrib.geography.models import Geography
11
+ from wbcore.contrib.geography.serializers import CountryRepresentationSerializer
12
+ from wbfdm.models import Classification, Instrument, InstrumentType
13
+ from wbfdm.serializers import (
14
+ ClassificationRepresentationSerializer,
15
+ InstrumentTypeRepresentationSerializer,
16
+ )
17
+ from wbportfolio.pms.typing import Portfolio as PortfolioDTO
18
+
19
+ from .mixins import ActivePortfolioRelationshipMixin
20
+
21
+
22
+ @register("Exposure Portfolio Rule Backend", rule_group_key="portfolio")
23
+ class RuleBackend(
24
+ ActivePortfolioRelationshipMixin,
25
+ ):
26
+ class GroupbyChoices(models.TextChoices):
27
+ UNDERLYING_INSTRUMENT = "underlying_instrument", "Underlying Instrument"
28
+ ASSET_TYPE = "instrument_type", "Asset Type"
29
+ CASH = "is_cash", "Cash"
30
+ CURRENCY = "currency", "Currency"
31
+ COUNTRY = "country", "Country"
32
+ PRIMARY_CLASSIFICATION = "primary_classification", "Primary Classification"
33
+ FAVORITE_CLASSIFICATION = "favorite_classification", "Favorite Classification"
34
+
35
+ class Field(models.TextChoices):
36
+ WEIGHTING = "weighting", "Weighting"
37
+ MARKET_CAPITALIZATION_USD = "market_capitalization_usd", "Market Capitalization (USD)"
38
+ MARKET_SHARE = "market_share", "Market Shares"
39
+ DAILY_LIQUIDITY = "daily_liquidity", "Daily Liquidity"
40
+ VOLUME_USD = "volume_usd", "Dollar Volume"
41
+
42
+ def __init__(self, *args, **kwargs):
43
+ super().__init__(*args, **kwargs)
44
+ self.group_by = self.GroupbyChoices(self.group_by)
45
+ self.field = self.Field(self.field)
46
+
47
+ @classmethod
48
+ def get_serializer_class(cls) -> wb_serializers.Serializer:
49
+ class RuleBackendSerializer(wb_serializers.Serializer):
50
+ group_by = wb_serializers.ChoiceField(
51
+ choices=cls.GroupbyChoices.choices,
52
+ default=cls.GroupbyChoices.ASSET_TYPE,
53
+ allow_null=True,
54
+ help_text="Choose how the position will be aggregated before evaluating the rule",
55
+ label="Group By",
56
+ )
57
+ field = wb_serializers.ChoiceField(
58
+ choices=cls.Field.choices,
59
+ default=cls.Field.WEIGHTING,
60
+ allow_null=True,
61
+ label="Field",
62
+ help_text="Choose which field will be evaluated after aggregatation",
63
+ )
64
+ is_cash = wb_serializers.BooleanField(
65
+ default=None, allow_null=True, label="Cash", help_text="Exclude cash position"
66
+ )
67
+ asset_classes = wb_serializers.PrimaryKeyRelatedField(
68
+ queryset=InstrumentType.objects.all(),
69
+ many=True,
70
+ default=None,
71
+ allow_null=True,
72
+ label="Only Asset Classes",
73
+ )
74
+ _asset_classes = InstrumentTypeRepresentationSerializer(source="asset_classes", many=True)
75
+
76
+ currencies = wb_serializers.PrimaryKeyRelatedField(
77
+ queryset=Currency.objects.all(),
78
+ many=True,
79
+ default=None,
80
+ allow_null=True,
81
+ label="Only Currencies",
82
+ )
83
+ _currencies = CurrencyRepresentationSerializer(many=True, source="parameters__currencies")
84
+ countries = wb_serializers.PrimaryKeyRelatedField(
85
+ queryset=Geography.countries.all(),
86
+ many=True,
87
+ default=None,
88
+ allow_null=True,
89
+ label="Only Countries",
90
+ )
91
+ _countries = CountryRepresentationSerializer(
92
+ many=True, source="parameters__countries", filter_params={"level": 1}
93
+ )
94
+ classifications = wb_serializers.PrimaryKeyRelatedField(
95
+ queryset=Classification.objects.all(),
96
+ many=True,
97
+ default=None,
98
+ allow_null=True,
99
+ label="Classifications",
100
+ )
101
+ _classifications = ClassificationRepresentationSerializer(many=True, source="parameters__classifications")
102
+
103
+ @classmethod
104
+ def get_parameter_fields(cls):
105
+ return [
106
+ "group_by",
107
+ "field",
108
+ "is_cash",
109
+ "asset_classes",
110
+ "currencies",
111
+ "countries",
112
+ "classifications",
113
+ ]
114
+
115
+ return RuleBackendSerializer
116
+
117
+ @property
118
+ def report_details(self) -> dict[str, str]:
119
+ repr = {
120
+ "Field": self.field.label,
121
+ "Group By": self.group_by.label,
122
+ }
123
+ if self.is_cash:
124
+ repr["Only Cash"] = "True"
125
+ if self.asset_classes:
126
+ repr["Only Types"] = ", ".join(map(lambda o: o.name, self.asset_classes))
127
+ if self.currencies:
128
+ repr["Only Currencies"] = ", ".join(map(lambda o: o.key, self.currencies))
129
+ if self.countries:
130
+ repr["Only Countries"] = ", ".join(map(lambda o: o.code_2, self.countries))
131
+ if self.classifications:
132
+ repr["Only Classifications"] = ", ".join(map(lambda o: o.name, self.classifications))
133
+ return repr
134
+
135
+ def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
136
+ if not (df := self._filter_df(portfolio.to_df())).empty:
137
+ df = df[[self.group_by.value, self.field.value]].groupby(self.group_by.value).sum().astype(float)
138
+ for threshold in self.thresholds:
139
+ dff = df.copy()
140
+ numerical_range = threshold.numerical_range
141
+ dff = dff[(dff[self.field.value] > numerical_range[0]) & (dff[self.field.value] < numerical_range[1])]
142
+ if not dff.empty:
143
+ for id, row in dff.to_dict("index").items():
144
+ obj, obj_repr = self._get_obj_repr(id)
145
+ severity: RiskIncidentType = threshold.severity
146
+ if self.field == self.Field.WEIGHTING:
147
+ breached_value = f"{row[self.field.value]:+,.2%}"
148
+ else:
149
+ breached_value = f"{row[self.field.value]:,.3f}"
150
+ if row[self.field.value] < 0:
151
+ breached_value = f'<span style="color:red">{breached_value}</span>'
152
+ else:
153
+ breached_value = f'<span style="color:green">{breached_value}</span>'
154
+ yield backend.IncidentResult(
155
+ breached_object=obj,
156
+ breached_object_repr=obj_repr,
157
+ breached_value=breached_value,
158
+ report_details=self.report_details,
159
+ severity=severity,
160
+ )
161
+
162
+ def _filter_df(self, df):
163
+ if df.empty:
164
+ return df
165
+ if self.is_cash is True or self.is_cash is False:
166
+ df = df[df["is_cash"] == self.is_cash]
167
+
168
+ if self.asset_classes:
169
+ df = df[df["instrument_type"].isin(list(map(lambda o: o.id, self.asset_classes)))]
170
+ if self.countries:
171
+ df = df[(~df["country"].isnull() & df["country"].isin(list(map(lambda o: o.id, self.countries))))]
172
+ if self.currencies:
173
+ df = df[(~df["currency"].isnull() & df["currency"].isin(list(map(lambda o: o.id, self.currencies))))]
174
+ if self.classifications:
175
+ df = df[
176
+ (
177
+ ~df["primary_classification"].isnull()
178
+ & df["primary_classification"].isin(list(map(lambda o: o.id, self.classifications)))
179
+ )
180
+ ]
181
+ return df
182
+
183
+ def _get_obj_repr(self, pivot_object_id) -> tuple[models.Model | None, str]:
184
+ match self.group_by:
185
+ case self.GroupbyChoices.UNDERLYING_INSTRUMENT:
186
+ obj = Instrument.objects.get(id=pivot_object_id)
187
+ return obj, str(obj)
188
+ case self.GroupbyChoices.ASSET_TYPE:
189
+ return None, InstrumentType.objects.get(id=pivot_object_id)
190
+ case self.GroupbyChoices.CASH:
191
+ return None, "Cash"
192
+ case self.GroupbyChoices.COUNTRY:
193
+ obj = Geography.countries.get(id=pivot_object_id)
194
+ return obj, str(obj)
195
+ case self.GroupbyChoices.CURRENCY:
196
+ obj = Currency.objects.get(id=pivot_object_id)
197
+ return obj, str(obj)
198
+ case self.GroupbyChoices.PRIMARY_CLASSIFICATION:
199
+ obj = Classification.objects.get(id=pivot_object_id)
200
+ return obj, str(obj)
201
+ case self.GroupbyChoices.FAVORITE_CLASSIFICATION:
202
+ obj = Classification.objects.get(id=pivot_object_id)
203
+ return obj, str(obj)
@@ -0,0 +1,89 @@
1
+ from typing import Generator
2
+
3
+ from django.db.models import Q
4
+ from wbcompliance.models.risk_management import backend
5
+ from wbcompliance.models.risk_management.dispatch import register
6
+ from wbcore import serializers as wb_serializers
7
+ from wbfdm.models import Instrument, InstrumentList, InstrumentListThroughModel
8
+ from wbfdm.serializers.instruments.instrument_lists import (
9
+ InstrumentListRepresentationSerializer,
10
+ )
11
+ from wbportfolio.pms.typing import Portfolio as PortfolioDTO
12
+
13
+ from .mixins import ActivePortfolioRelationshipMixin
14
+
15
+
16
+ @register("Instrument List Portfolio Rule Backend", rule_group_key="portfolio")
17
+ class RuleBackend(ActivePortfolioRelationshipMixin):
18
+ def __init__(self, *args, **kwargs):
19
+ super().__init__(*args, **kwargs)
20
+ if self.instrument_list_type:
21
+ self.instrument_lists = InstrumentList.objects.filter(instrument_list_type=self.instrument_list_type)
22
+ self.instruments_relationship = InstrumentListThroughModel.objects.filter(
23
+ Q(instrument_list__in=self.instrument_lists)
24
+ & (Q(from_date__isnull=True) | Q(from_date__lte=self.evaluation_date))
25
+ & (Q(to_date__isnull=True) | Q(to_date__gt=self.evaluation_date))
26
+ )
27
+ self.instrument_lists_repr = " ,".join(map(lambda x: x.name, self.instrument_lists))
28
+ self.severity = self.thresholds[0].severity
29
+
30
+ @classmethod
31
+ def get_serializer_class(cls) -> wb_serializers.Serializer:
32
+ class RuleBackendSerializer(wb_serializers.Serializer):
33
+ exclude = wb_serializers.BooleanField(
34
+ default=True,
35
+ label="Exclude",
36
+ help_text="If true, the rule will check that the portfolio composition DOES NOT intersect the given instrument list",
37
+ )
38
+ instrument_list_type = wb_serializers.ChoiceField(
39
+ choices=InstrumentList.InstrumentListType.choices,
40
+ required=False,
41
+ default=None,
42
+ allow_null=True,
43
+ help_text="If specified, will dynamically load the list of instrument list to check of the same specified type",
44
+ label="Instrument List Type",
45
+ )
46
+ instrument_lists = wb_serializers.PrimaryKeyRelatedField(
47
+ queryset=InstrumentList.objects.all(),
48
+ many=True,
49
+ default=None,
50
+ allow_null=True,
51
+ label="Instrument Lists",
52
+ )
53
+ _instrument_lists = InstrumentListRepresentationSerializer(many=True)
54
+
55
+ @classmethod
56
+ def get_parameter_fields(cls):
57
+ return [
58
+ "exclude",
59
+ "instrument_list_type",
60
+ "instrument_lists",
61
+ ]
62
+
63
+ return RuleBackendSerializer
64
+
65
+ def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
66
+ for instrument_id, weight in portfolio.positions_map.items():
67
+ instrument = Instrument.objects.get(id=instrument_id)
68
+ relationships = self.instruments_relationship.filter(instrument=instrument, validated=True)
69
+
70
+ if self.exclude and relationships.exists():
71
+ report_details = {
72
+ "Instrument Lists": ", ".join(relationships.values_list("instrument_list__name", flat=True)),
73
+ }
74
+ yield backend.IncidentResult(
75
+ breached_object=instrument,
76
+ breached_object_repr=str(instrument),
77
+ breached_value=f"# {relationships.count()}",
78
+ report_details=report_details,
79
+ severity=self.severity,
80
+ )
81
+ elif not self.exclude and not relationships.exists():
82
+ report_details = {"Instrument Lists": self.instrument_lists_repr}
83
+ yield backend.IncidentResult(
84
+ breached_object=instrument,
85
+ breached_object_repr=str(instrument),
86
+ breached_value=f"# {relationships.count()}",
87
+ report_details=report_details,
88
+ severity=self.severity,
89
+ )
@@ -0,0 +1,86 @@
1
+ from contextlib import suppress
2
+ from math import ceil
3
+ from typing import Generator
4
+
5
+ from django.contrib.contenttypes.models import ContentType
6
+ from django.db import models
7
+ from django.db.models import Exists, OuterRef, Sum
8
+ from wbcompliance.models.risk_management import backend
9
+ from wbcompliance.models.risk_management.dispatch import register
10
+ from wbcore import serializers as wb_serializers
11
+ from wbfdm.models import Instrument, InstrumentPrice
12
+ from wbportfolio.models import AssetPosition
13
+
14
+
15
+ @register("Liquidity Risk", rule_group_key="portfolio")
16
+ class RuleBackend(backend.AbstractRuleBackend):
17
+ OBJECT_FIELD_NAME: str = "instrument"
18
+
19
+ instrument: Instrument
20
+
21
+ def get_queryset(self):
22
+ return self.instrument.assets.filter(
23
+ date=self.evaluation_date, shares__isnull=False, is_invested=True
24
+ ).exclude(shares=0)
25
+
26
+ def is_passive_evaluation_valid(self) -> bool:
27
+ return self.get_queryset().exists()
28
+
29
+ @classmethod
30
+ def get_allowed_content_type(cls) -> "ContentType":
31
+ return ContentType.objects.get_for_model(Instrument)
32
+
33
+ def _build_dto_args(self):
34
+ with suppress(InstrumentPrice.DoesNotExist):
35
+ last_price = self.instrument.valuations.get(date=self.evaluation_date)
36
+ total_shares = self.get_queryset().aggregate(c=Sum("shares"))["c"]
37
+ if last_price.volume_50d and total_shares:
38
+ return float(total_shares), last_price.volume_50d
39
+ return tuple()
40
+
41
+ @classmethod
42
+ def get_all_active_relationships(cls) -> models.QuerySet:
43
+ try:
44
+ base_qs = AssetPosition.objects.filter(shares__isnull=False, is_invested=True).exclude(shares=0)
45
+ last_asset_position = base_qs.latest("date").date
46
+
47
+ return Instrument.objects.annotate(
48
+ has_assets=Exists(base_qs.filter(underlying_instrument=OuterRef("pk"), date=last_asset_position))
49
+ ).filter(has_assets=True, children__isnull=True)
50
+ except AssetPosition.DoesNotExist:
51
+ return Instrument.objects.none()
52
+
53
+ @classmethod
54
+ def get_serializer_class(cls) -> wb_serializers.Serializer:
55
+ class RuleBackendSerializer(wb_serializers.Serializer):
56
+ liquidation_factor = wb_serializers.FloatField(default=3.0, label="Liquidation Factor")
57
+ redemption_pct = wb_serializers.FloatField(default=0.80, label="Redemption Percentage", percent=True)
58
+
59
+ @classmethod
60
+ def get_parameter_fields(cls):
61
+ return [
62
+ "below_x_days",
63
+ "redemption_pct",
64
+ ]
65
+
66
+ return RuleBackendSerializer
67
+
68
+ def _process_dto(
69
+ self, total_shares: float, volume_50d: float, **kwargs
70
+ ) -> Generator[backend.IncidentResult, None, None]:
71
+ days_to_liquidate = (total_shares * self.redemption_pct * self.liquidation_factor) / volume_50d
72
+ for threshold in self.thresholds:
73
+ numerical_range = threshold.numerical_range
74
+ if days_to_liquidate >= numerical_range[0] and days_to_liquidate < numerical_range[1]:
75
+ yield backend.IncidentResult(
76
+ breached_object=self.instrument,
77
+ breached_object_repr=str(self.instrument),
78
+ breached_value=f"{ceil(days_to_liquidate)} Days",
79
+ report_details={
80
+ "Volume 50D": volume_50d,
81
+ "Total Shares": total_shares,
82
+ "Redemption Percentage": f"{self.redemption_pct:.1%}",
83
+ "Liquidation Factor": self.liquidation_factor,
84
+ },
85
+ severity=threshold.severity,
86
+ )