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,1307 @@
1
+ from datetime import date
2
+ from typing import Optional
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ import plotly.graph_objects as go
7
+ from pandas.tseries.offsets import BDay
8
+ from plotly.subplots import make_subplots
9
+ from wbcore.contrib.currency.models import CurrencyFXRates
10
+ from wbfdm.enums import MarketData
11
+ from wbfdm.models import Instrument as InstrumentFDM
12
+ from wbfdm.models.instruments.instrument_prices import InstrumentPrice
13
+
14
+
15
+ class LiquidityStressMixin:
16
+ def get_product_ids_from_group_product_or_product(self) -> [None, list]:
17
+ """
18
+ The function returns a list of id(s) if:
19
+ - a simple product id if instrument_type == Product.
20
+ - the list of the product ids if instrument_type == ProductGroup.
21
+ - the list of the products ids if instrument_type == Product and if the product belongs to a group.
22
+ Otherwise the function returns None.
23
+ """
24
+
25
+ if self.instrument_type not in ["Product", "ProductGroup"]:
26
+ return None
27
+ products_id = [self.pk] # if self is a Product and has not a self.group, it keeps this value.
28
+ casted_instrument = self.get_casted_instrument()
29
+ if self.instrument_type.key == "product_group":
30
+ products_id = casted_instrument.products.values_list("id", flat=True)
31
+ elif self.instrument_type.key == "product" and casted_instrument.group:
32
+ products_id = casted_instrument.group.products.values_list("id", flat=True)
33
+
34
+ return products_id
35
+
36
+ @staticmethod
37
+ def waterfall_and_slicing_calculation(
38
+ mean_volume_by_id: pd.Series, price_change: float, df_asset_positions: pd.DataFrame, liq_factor: float
39
+ ) -> pd.DataFrame:
40
+ df = pd.DataFrame(index=df_asset_positions.index, columns=["waterfall", "slicing"])
41
+ df["waterfall"] = df_asset_positions.shares.div(mean_volume_by_id * liq_factor, level="instrument")
42
+ df["waterfall"] = df["waterfall"].mask(df["waterfall"] > 360, 360)
43
+ stock_index = df.loc[:, "waterfall"].dropna().index
44
+ df.loc[stock_index, "slicing"] = 1 / ((1 + price_change) / df["waterfall"]).loc[("Equity", slice(None))].min()
45
+ return df
46
+
47
+ def get_volumes_from_scenario_stress_test(
48
+ self, weights_date: date, df_volumes: pd.DataFrame, df_asset_positions: pd.DataFrame, liq_factor: float
49
+ ) -> pd.DataFrame:
50
+ # Depending on the scenario, we slice the volume DataFrame for the corresponding dates.
51
+ scenarios = [
52
+ "Baseline Scenario",
53
+ "COVID-19",
54
+ "Lehman",
55
+ "Lehman Spring",
56
+ "Debt Crisis",
57
+ "China Crisis",
58
+ "Dotcom",
59
+ "Volume falls by 60 pct",
60
+ ]
61
+
62
+ def get_volume_period_scenario(scenario_name: str):
63
+ import datetime as dt
64
+
65
+ from pandas.tseries.offsets import BDay
66
+
67
+ if scenario_name == "Baseline Scenario" or scenario_name == "Volume falls by 60 pct":
68
+ start, end = (weights_date - BDay(50)).date(), weights_date
69
+
70
+ elif scenario_name == "COVID-19":
71
+ start, end = dt.date(2021, 2, 10), dt.date(2021, 3, 23)
72
+
73
+ elif scenario_name == "Lehman":
74
+ start, end = dt.date(2008, 10, 3), dt.date(2008, 10, 10)
75
+
76
+ elif scenario_name == "Lehman Spring":
77
+ start, end = dt.date(2009, 2, 16), dt.date(2009, 2, 23)
78
+
79
+ elif scenario_name == "Debt Crisis":
80
+ start, end = dt.date(2011, 8, 1), dt.date(2011, 8, 11)
81
+
82
+ elif scenario_name == "China Crisis":
83
+ start, end = dt.date(2015, 8, 15), dt.date(2015, 8, 24)
84
+
85
+ elif scenario_name == "Dotcom":
86
+ start, end = dt.date(2002, 7, 15), dt.date(2002, 7, 22)
87
+
88
+ else:
89
+ return pd.DataFrame() # No existing scenario
90
+
91
+ df_volume_scenario = df_volumes.loc[:, start:end]
92
+ return df_volume_scenario
93
+
94
+ # Depending to the method, the "days to liquidate"'s result is different.
95
+ methods = ["waterfall", "slicing"]
96
+
97
+ # ---- SCENARIOS CALCULATION ---- :
98
+ price_change_factor = 0
99
+ multi_columns = pd.MultiIndex.from_product([scenarios, methods], names=["scenario", "method"])
100
+ days_to_liquidate = pd.DataFrame(index=df_asset_positions.index, columns=multi_columns)
101
+ liquidity_equivalent_one_day = pd.DataFrame(index=df_asset_positions.index, columns=scenarios)
102
+
103
+ for scenario in scenarios:
104
+ volume_scenario = get_volume_period_scenario(scenario)
105
+ if volume_scenario.empty:
106
+ continue
107
+ mean_volume = volume_scenario.mean(axis=1)
108
+ if scenario == "Volume falls by 60 pct":
109
+ mean_volume *= 0.4
110
+
111
+ days_to_liquidate.loc[:, (scenario, methods)] = self.waterfall_and_slicing_calculation(
112
+ mean_volume, price_change_factor, df_asset_positions, liq_factor
113
+ ).values
114
+ liquidity_equivalent_one_day[scenario] = (
115
+ (1 + price_change_factor)
116
+ * df_asset_positions.total_value_usd
117
+ / days_to_liquidate.loc[(slice(None), mean_volume.index), (scenario, "waterfall")]
118
+ )
119
+ return days_to_liquidate
120
+
121
+ @staticmethod
122
+ def get_percentile_worst_dollar_volume(df_dollar_volume: pd.DataFrame, method: str = "mean_below_worst"):
123
+ list_pct_worst_volume = range(0, 101)
124
+ multi_index = pd.MultiIndex.from_product(
125
+ [list_pct_worst_volume, df_dollar_volume.index.values], names=["x%_worst_volume", "instrument"]
126
+ )
127
+ average_worst_dollar_volume = pd.Series(index=multi_index, dtype=float)
128
+ for pct_worst_volume in list_pct_worst_volume:
129
+ if method == "mean_below_worst":
130
+ average_worst_dollar_volume.loc[(pct_worst_volume, slice(None))] = (
131
+ df_dollar_volume.where(
132
+ df_dollar_volume.le(
133
+ df_dollar_volume.quantile(pct_worst_volume / 100, axis=1, interpolation="midpoint"), axis=0
134
+ )
135
+ )
136
+ .mean(axis=1)
137
+ .values
138
+ )
139
+ else:
140
+ average_worst_dollar_volume.loc[(pct_worst_volume, slice(None))] = df_dollar_volume.quantile(
141
+ pct_worst_volume / 100, axis=1, interpolation="midpoint"
142
+ ).values
143
+ return average_worst_dollar_volume
144
+
145
+ @staticmethod
146
+ def reverse_stress_test(
147
+ total_aum: float,
148
+ df_asset_positions: pd.DataFrame,
149
+ average_worst_dollar_volume: pd.Series,
150
+ liq_factor: float,
151
+ below_x_days: int = 5,
152
+ ) -> pd.DataFrame:
153
+ # Remove cash instruments from the instruments liquidity estimation
154
+ cash_instruments = df_asset_positions.loc[("Cash", slice(None))].index.unique("instrument")
155
+ instruments_id = average_worst_dollar_volume.index.unique("instrument")
156
+ instruments_id = instruments_id.drop(cash_instruments)
157
+
158
+ x_pct = average_worst_dollar_volume.index.unique(0)
159
+ aum_multiplication = range(1, 51)
160
+ multi_index = pd.MultiIndex.from_product(
161
+ [instruments_id, x_pct, x_pct],
162
+ names=["instrument", "x%_redemption", "x%_worst_volume"],
163
+ )
164
+ df = pd.DataFrame(index=multi_index)
165
+
166
+ df = df.join(pd.DataFrame(average_worst_dollar_volume, columns=["average_worst_dollar_volume"]))
167
+ df = df.join(df_asset_positions.weighting.droplevel("type"), on="instrument")
168
+ for multiplication in aum_multiplication:
169
+ amount_to_liquidate = (
170
+ total_aum * multiplication * df.index.get_level_values("x%_redemption") / 100 * df["weighting"]
171
+ )
172
+ df[f"days_to_liquidate {multiplication}"] = amount_to_liquidate / (
173
+ liq_factor * df["average_worst_dollar_volume"]
174
+ )
175
+
176
+ df_days_to_liquidate = df.filter(like="days_to_liquidate")
177
+ df_weights_sold = (1 / df_days_to_liquidate).mul(df["weighting"], axis="index") * below_x_days
178
+
179
+ # TODO: cannot use mask on series because it does not work with pandas 1.5.3.
180
+ weights = df.weighting.to_frame(name=df_weights_sold.columns[0])
181
+ weights = weights.reindex(df_weights_sold.columns, axis=1).ffill(axis=1)
182
+ df_weights_sold = df_weights_sold.mask(df_weights_sold > weights, weights)
183
+ df_weights_sold = df_weights_sold.groupby(["x%_redemption", "x%_worst_volume"]).sum()
184
+ df = df_weights_sold.unstack(level=0)
185
+
186
+ # Add cash weights
187
+ cash = df_asset_positions.loc[("Cash", slice(None))].weighting.sum()
188
+ df += cash
189
+ return df
190
+
191
+ def stress_volume_bid_ask_test(
192
+ self,
193
+ df_asset_positions: pd.DataFrame,
194
+ df_dollar_volume: pd.DataFrame,
195
+ df_volumes: pd.DataFrame,
196
+ df_bid_ask_spread: pd.DataFrame,
197
+ liq_factor: float,
198
+ pct_worst_dollar_volume: float = 0.1,
199
+ pct_worst_volume: float = 0.1,
200
+ pct_higher_bid_ask_spread: float = 0.9,
201
+ price_change_factor: float = 0,
202
+ acceptable_loss: float = 0.3,
203
+ ) -> pd.DataFrame:
204
+ worst_dollar_volumes = df_dollar_volume.where(
205
+ df_dollar_volume.le(df_dollar_volume.quantile(pct_worst_dollar_volume, axis=1), axis=0)
206
+ )
207
+
208
+ # volumes:
209
+ worst_corresponding_volumes = df_volumes.where(worst_dollar_volumes.notnull())
210
+ worst_volumes = worst_corresponding_volumes.where(
211
+ worst_corresponding_volumes.le(worst_corresponding_volumes.quantile(pct_worst_volume, axis=1), axis=0)
212
+ )
213
+ mean_worst_volumes = worst_volumes.mean(axis=1)
214
+
215
+ scenarios = ["S-T Worst Volume", "S-T Worst B-A"]
216
+ methods = ["waterfall", "slicing"]
217
+ multi_columns = pd.MultiIndex.from_product([scenarios, methods], names=["scenario", "method"])
218
+ days_to_liquidate = pd.DataFrame(index=df_asset_positions.index, columns=multi_columns)
219
+
220
+ days_to_liquidate.loc[:, ("S-T Worst Volume", methods)] = self.waterfall_and_slicing_calculation(
221
+ mean_worst_volumes, price_change_factor, df_asset_positions, liq_factor
222
+ ).values
223
+
224
+ # bid-ask spread:
225
+ worst_corresponding_bid_ask_spread = df_bid_ask_spread.where(worst_dollar_volumes.notnull())
226
+ worst_bid_ask_spread = worst_corresponding_bid_ask_spread.where(
227
+ worst_corresponding_bid_ask_spread.ge(
228
+ worst_corresponding_bid_ask_spread.quantile(pct_higher_bid_ask_spread, axis=1), axis=0
229
+ )
230
+ )
231
+
232
+ # we do not use the function to calculate waterfall here because formula is different.
233
+ days_to_liquidate.loc[(slice(None), worst_bid_ask_spread.index), ("S-T Worst B-A", "waterfall")] = (
234
+ worst_bid_ask_spread.mean(axis=1) / acceptable_loss
235
+ ).values
236
+ days_to_liquidate.loc[(slice(None), worst_bid_ask_spread.index), ("S-T Worst B-A", "slicing")] = (
237
+ 1 / ((1 + price_change_factor) / days_to_liquidate.loc[:, ("S-T Worst B-A", "waterfall")]).min()
238
+ )
239
+
240
+ return days_to_liquidate
241
+
242
+ @staticmethod
243
+ def aggregate_days_to_liquidate(days_to_liquidate: pd.DataFrame) -> pd.DataFrame:
244
+ # Aggregation:
245
+ instrument_types = days_to_liquidate.index.get_level_values("type").drop_duplicates()
246
+ timeline = ["1 day or less", "2-7 days", "8-15 days", "16-30 days", "31-60 days", "61-180 days"]
247
+ df_aggregate = pd.DataFrame(
248
+ index=pd.MultiIndex.from_product([instrument_types, timeline], names=["type", "time"]),
249
+ columns=days_to_liquidate.drop("instrument", axis=1, level=0).columns,
250
+ )
251
+ _dict = {
252
+ "1 day or less": [0, 1],
253
+ "2-7 days": [1, 7],
254
+ "8-15 days": [7, 15],
255
+ "16-30 days": [15, 30],
256
+ "31-60 days": [30, 60],
257
+ "61-180 days": [60, 180],
258
+ }
259
+ df_waterfall = days_to_liquidate.loc[:, (slice(None), "waterfall")]
260
+ df_slicing = days_to_liquidate.loc[:, (slice(None), "slicing")]
261
+ type_index = pd.Index(instrument_types)
262
+ for period, value in _dict.items():
263
+ tmp_waterfall = df_waterfall.where((df_waterfall >= value[0]) & (df_waterfall <= value[1]))
264
+ for scenario in days_to_liquidate.drop(("instrument", "weighting"), axis=1).columns:
265
+ all_index = days_to_liquidate.loc[:, scenario].dropna().index
266
+ total_weight = days_to_liquidate.loc[all_index, ("instrument", "weighting")]
267
+ if scenario[1] == "waterfall":
268
+ selection_idx = tmp_waterfall.loc[:, scenario].dropna().index
269
+ total_weight = total_weight.sum()
270
+ sliced_weight = (
271
+ days_to_liquidate.loc[selection_idx, ("instrument", "weighting")]
272
+ .groupby("type")
273
+ .sum()
274
+ .reindex(type_index)
275
+ )
276
+ df_aggregate.loc[(slice(None), period), scenario] = (sliced_weight / total_weight).values
277
+ else: # slicing
278
+ tmp_slicing = df_slicing.loc[:, scenario]
279
+ total_weight = total_weight.groupby("type").sum() / total_weight.sum()
280
+ total_weight.name = ("instrument", "agg_weights")
281
+ df_aggregate.loc[(slice(None), period), scenario] = (
282
+ total_weight / tmp_slicing.groupby("type").max() * value[1]
283
+ ).values
284
+ df_aggregate = df_aggregate.join(total_weight)
285
+ df_aggregate.loc[(slice(None), period), scenario] = df_aggregate.loc[
286
+ (slice(None), period), scenario
287
+ ].mask(
288
+ (
289
+ df_aggregate.loc[(slice(None), period), scenario]
290
+ > df_aggregate.loc[(slice(None), period), ("instrument", "agg_weights")]
291
+ )
292
+ | (df_aggregate.loc[(slice(None), period), scenario] == -np.inf),
293
+ df_aggregate.loc[(slice(None), period), ("instrument", "agg_weights")],
294
+ )
295
+ df_aggregate.drop(("instrument", "agg_weights"), axis=1, inplace=True)
296
+ df_aggregate.loc[:, (slice(None), "waterfall")] = (
297
+ df_aggregate.loc[:, (slice(None), "waterfall")].fillna(0).groupby("type").cumsum()
298
+ )
299
+ df_aggregate_portfolio = df_aggregate.stack("method").groupby(["time", "method"]).sum()
300
+ df_aggregate_portfolio = pd.concat({"Portfolio": df_aggregate_portfolio}, names=["type"]).unstack("method")
301
+ df_aggregate_portfolio = df_aggregate_portfolio.reindex(labels=timeline, level="time")
302
+ df_aggregate = pd.concat([df_aggregate_portfolio, df_aggregate])
303
+ return df_aggregate
304
+
305
+ @staticmethod
306
+ def series_of_colors(portfolio_value: pd.Series) -> pd.DataFrame:
307
+ portfolio_value_colors = pd.DataFrame(columns=["colors", "message"])
308
+ portfolio_value_colors.loc[:, "colors"] = portfolio_value.astype(float).round(4)
309
+ for k, v in portfolio_value_colors.colors.items():
310
+ v_adj = round(v * 100, 2)
311
+ if k == "1 day or less":
312
+ portfolio_value_colors.at[k, "colors"] = "#0FFBA6" if v >= 0.7 else "#FBE426"
313
+ portfolio_value_colors.at[k, "message"] = (
314
+ pd.NA if v >= 0.7 else f"to liquidate {v_adj}%, more than 1 day is needed"
315
+ )
316
+ elif k == "2-7 days":
317
+ portfolio_value_colors.at[k, "colors"] = "#0FFBA6" if v >= 0.8 else "#FBE426"
318
+ portfolio_value_colors.at[k, "message"] = (
319
+ pd.NA if v >= 0.8 else f"to liquidate {v_adj}%, more than 7 days are needed"
320
+ )
321
+ elif k == "8-15 days":
322
+ portfolio_value_colors.at[k, "colors"] = (
323
+ "#0FFBA6" if v >= 0.9 else "#FBE426" if v >= 0.7 else "#FC6955"
324
+ )
325
+ portfolio_value_colors.at[k, "message"] = (
326
+ pd.NA if v >= 0.9 else f"to liquidate {v_adj}%, more than 15 days are needed"
327
+ )
328
+ elif k == "16-30 days":
329
+ portfolio_value_colors.at[k, "colors"] = "#0FFBA6" if v >= 1 else "#FBE426" if v >= 0.9 else "#FC6955"
330
+ portfolio_value_colors.at[k, "message"] = (
331
+ pd.NA if v >= 1 else f"to liquidate {v_adj}%, more than 30 days are needed"
332
+ )
333
+ elif k == "31-60 days":
334
+ portfolio_value_colors.at[k, "colors"] = "#0FFBA6" if v >= 1 else "#FBE426" if v >= 0.95 else "#FC6955"
335
+ portfolio_value_colors.at[k, "message"] = (
336
+ pd.NA if v >= 1 else f"to liquidate {v_adj}%, more than 60 days are needed"
337
+ )
338
+ else:
339
+ portfolio_value_colors.at[k, "colors"] = "#0FFBA6" if v >= 1 else "#FBE426" if v >= 0.99 else "#FC6955"
340
+ portfolio_value_colors.at[k, "message"] = (
341
+ pd.NA if v >= 1 else f"to liquidate {v_adj}%, more than 180 days are needed"
342
+ )
343
+ return portfolio_value_colors
344
+
345
+ def liquidity_monitor_graph(self, portfolio_value: pd.Series, expectation_net_redemption_df: pd.DataFrame):
346
+ index = portfolio_value.index
347
+ # portfolio_value = df_aggregate.loc[("Portfolio", slice(None)), (scenario, method)].droplevel(0, axis=0)
348
+ portfolio_value_colors = self.series_of_colors(portfolio_value)
349
+
350
+ portfolio_value_list = portfolio_value.mul(100).values.tolist()
351
+ expectation = expectation_net_redemption_df.mul(100).values.tolist()
352
+ fig = make_subplots(rows=1, cols=2, subplot_titles=("Liquidity Balance Overview", "Redemption Coverage Ratio"))
353
+ fig.add_bar(
354
+ x=index,
355
+ y=portfolio_value_list,
356
+ row=1,
357
+ col=1,
358
+ marker=dict(color=portfolio_value_colors.colors),
359
+ name=portfolio_value.name[1],
360
+ )
361
+ fig.add_bar(
362
+ x=index,
363
+ y=expectation,
364
+ row=1,
365
+ col=1,
366
+ marker=dict(color="rgb(179, 179, 179)"),
367
+ name="Expected Net Redemption",
368
+ )
369
+ fig.update_yaxes(ticksuffix="%", row=1, col=1, range=[0, 108])
370
+ fig.update_traces(texttemplate="%{value}", row=1, col=1)
371
+
372
+ rcr = pd.Series(portfolio_value_list, index=index) / pd.Series(expectation, index=index) * 100
373
+ rcr = rcr.mask(rcr > 200, 200.00001)
374
+ rcr_colors = rcr.map(lambda x: "#0FFBA6" if x > 200 else "#FC6955" if x < 120 else "#FBE426")
375
+ fig.add_bar(
376
+ x=rcr,
377
+ y=index,
378
+ row=1,
379
+ col=2,
380
+ marker_color=rcr_colors,
381
+ orientation="h",
382
+ text=[f"{val: .0f}%" if val < 200 else f">{val: .0f}%" for val in rcr],
383
+ )
384
+ fig["data"][2].width = 0.6
385
+ fig["data"][2]["showlegend"] = False
386
+ fig.update_xaxes(ticksuffix="%", range=[0, 225], row=1, col=2)
387
+
388
+ fig.update_traces(textposition="outside")
389
+ fig.update_layout(
390
+ barmode="group",
391
+ font_size=12,
392
+ uniformtext_mode="hide",
393
+ title_font_size=20,
394
+ yaxis_title="Percent (%)",
395
+ legend=dict(orientation="h", yanchor="bottom", y=1.1, xanchor="left"),
396
+ )
397
+ return fig
398
+
399
+ @staticmethod
400
+ def asset_liquidity_graph(df_aggregate, scenario, method):
401
+ pd.options.plotting.backend = "plotly"
402
+ df = (df_aggregate.loc[:, (scenario, method)] * 100).astype(float).round(2)
403
+ fig = make_subplots(
404
+ rows=1,
405
+ cols=2,
406
+ specs=[[{"type": "bar"}, {"type": "table"}]],
407
+ subplot_titles=("Liquidity profile - Breakdown per asset type", "Available Resources (% NAV)"),
408
+ )
409
+ index_bar = df.index.drop("Portfolio", level=0) # for bar plot, we do not need portfolio (total) value
410
+ for instrument_type in index_bar.get_level_values(0).drop_duplicates()[::-1]:
411
+ series = df.loc[(instrument_type, slice(None))]
412
+ fig.add_bar(name=instrument_type, x=series.index, y=series.values, row=1, col=1)
413
+ fig.update_yaxes(ticksuffix="%", title="Percent (%)", row=1, col=1)
414
+
415
+ df_table = df.unstack() # Asset Type in index ; Time bucket in columns
416
+ df_table = df_table.reindex(df.index.get_level_values(1).drop_duplicates(), axis=1) # preserve columns order
417
+ df_table = df_table.reindex(df.index.get_level_values(0).drop_duplicates(), axis=0) # preserve index order
418
+ df_values = df_table.T.values.tolist()
419
+ df_values.insert(0, df_table.index.to_list()) # Index in values for the table.
420
+ fig.add_table(
421
+ header=dict(
422
+ values=df_table.columns.insert(0, "Asset Type").to_list(),
423
+ line_color="darkslategray",
424
+ fill_color="royalblue",
425
+ align=["left", "center"],
426
+ font=dict(color="white", size=12),
427
+ height=50,
428
+ ),
429
+ cells=dict(
430
+ values=df_values,
431
+ line_color="darkslategray",
432
+ align=["left", "center"],
433
+ font=dict(color="black", size=11),
434
+ suffix=[None] + ["%"] * 5,
435
+ fill=dict(color=["paleturquoise", ["lightgrey", "white"]]),
436
+ height=40,
437
+ ),
438
+ row=1,
439
+ col=2,
440
+ )
441
+
442
+ fig.update_layout(barmode="stack", legend=dict(orientation="h", yanchor="bottom", y=1.1, xanchor="left"))
443
+
444
+ return fig
445
+
446
+ @staticmethod
447
+ def liquidity_bucketing_graph(df_aggregate, scenario):
448
+ pd.options.plotting.backend = "plotly"
449
+ df = (
450
+ (df_aggregate.loc[("Portfolio", slice(None)), (scenario, slice(None))] * 100)
451
+ .astype(float)
452
+ .round(2)
453
+ .droplevel(0)
454
+ .droplevel(0, axis=1)
455
+ )
456
+ df = df.reindex(["waterfall", "slicing"], axis="columns")
457
+ df.rename(
458
+ index={
459
+ "1 day or less": "Very High Liquidity",
460
+ "2-7 days": "High Liquidity",
461
+ "8-15 days": "Medium Liquidity",
462
+ "16-30 days": "Low Liquidity",
463
+ "31-60 days": "Very Low Liquidity",
464
+ "61-180 days": "Almost No Liquidity",
465
+ },
466
+ inplace=True,
467
+ )
468
+
469
+ colors = ["green", "lightgreen", "lightblue", "#FC6955", "orange", "red"]
470
+ fig = make_subplots(rows=1, cols=2, subplot_titles=(None, "Delta"))
471
+ fig.add_traces(df.diff().fillna(df).abs().T.plot.bar()["data"], rows=1, cols=1)
472
+
473
+ fig.update_layout(
474
+ barmode="group", legend=dict(orientation="h", yanchor="bottom", y=1.1, xanchor="left"), font_size=10
475
+ )
476
+
477
+ for i in range(len(fig["data"])):
478
+ fig["data"][i]["marker"]["color"] = colors[i]
479
+
480
+ df = df["waterfall"] - df["slicing"]
481
+ fig2 = df.plot.bar()["data"][0]
482
+ fig2["marker_color"] = colors
483
+ fig2["showlegend"] = False
484
+ fig.add_trace(fig2, row=1, col=2)
485
+ fig.update_yaxes(ticksuffix="%", title="Percent (%)", range=[0, 105])
486
+
487
+ return fig
488
+
489
+ @staticmethod
490
+ def liability_liquidity_profile_expectations_graph(df_redemption):
491
+ new_index = ["1 day or less", "2-7 days", "8-15 days", "16-30 days", "31-60 days", "61-180 days"]
492
+
493
+ def get_expected_values(net=True):
494
+ col_filter = "net_perc_net_red" if net else "net_perc_gross_red"
495
+ expected_redemptions = pd.Series(
496
+ df_redemption.filter(like=col_filter).mean().sort_values().round(4).values, index=new_index
497
+ )
498
+ return expected_redemptions * 100
499
+
500
+ expected_net_redemption = get_expected_values(net=True)
501
+ expected_gross_redemption = get_expected_values(net=False)
502
+
503
+ fig = make_subplots(rows=1, cols=2, subplot_titles=("Expected Net Redemptions", "Expected Gross Redemptions"))
504
+ fig.add_trace(expected_net_redemption.plot.bar()["data"][0], row=1, col=1)
505
+ fig.add_trace(expected_gross_redemption.plot.bar()["data"][0], row=1, col=2)
506
+ fig.update_yaxes(ticksuffix="%", range=[0, 108])
507
+ fig.update_traces(textposition="outside", texttemplate="%{value}")
508
+ fig.update_layout(
509
+ font_size=12,
510
+ uniformtext_mode="hide",
511
+ title_font_size=20,
512
+ yaxis_title="Percent (%)",
513
+ showlegend=False,
514
+ legend_title="",
515
+ )
516
+
517
+ return fig
518
+
519
+ @staticmethod
520
+ def liability_liquidity_profile_metrics_graph(df_redemption):
521
+ fig = make_subplots(
522
+ rows=1,
523
+ cols=2,
524
+ specs=[[{"type": "table"}, {"type": "table"}]],
525
+ subplot_titles=("Net Redemptions", "Gross Redemptions"),
526
+ )
527
+ df = (df_redemption.filter(like="net_perc").max() * 100).round(1)
528
+ gross = df.filter(like="net_perc_gross").sort_values()
529
+ net = df.filter(like="net_perc_net").sort_values()
530
+ gross_cells = [
531
+ ["Max Daily", "Max Weekly", "Max 2 Weeks", "Max 1 Month", "Max 2 Months", "Max 6 Months"],
532
+ gross.values,
533
+ ]
534
+ net_cells = [
535
+ ["Max Daily", "Max Weekly", "Max 2 Weeks", "Max 1 Month", "Max 2 Months", "Max 6 Months"],
536
+ net.values,
537
+ ]
538
+ fig.add_table(
539
+ columnorder=[1, 2],
540
+ columnwidth=[100, 400],
541
+ header=dict(
542
+ values=["Liquidity Metrics", "Aggregate"],
543
+ line_color="darkslategray",
544
+ fill_color="royalblue",
545
+ align="center",
546
+ font=dict(color="white", size=12),
547
+ height=30,
548
+ ),
549
+ cells=dict(
550
+ values=net_cells,
551
+ line_color="darkslategray",
552
+ align="center",
553
+ font=dict(color="black", size=11),
554
+ suffix=[None] + ["%"],
555
+ fill=dict(color=["paleturquoise", "white"]),
556
+ height=30,
557
+ ),
558
+ row=1,
559
+ col=1,
560
+ )
561
+
562
+ fig.add_table(
563
+ columnorder=[1, 2],
564
+ columnwidth=[100, 400],
565
+ header=dict(
566
+ values=["Liquidity Metrics", "Aggregate"],
567
+ line_color="darkslategray",
568
+ fill_color="royalblue",
569
+ align="center",
570
+ font=dict(color="white", size=12),
571
+ height=30,
572
+ ),
573
+ cells=dict(
574
+ values=gross_cells,
575
+ line_color="darkslategray",
576
+ align="center",
577
+ font=dict(color="black", size=11),
578
+ suffix=[None] + ["%"],
579
+ fill=dict(color=["paleturquoise", "white"]),
580
+ height=30,
581
+ ),
582
+ row=1,
583
+ col=2,
584
+ )
585
+
586
+ return fig
587
+
588
+ @staticmethod
589
+ def liquidity_monitor_stress_testing_tables(df_aggregate, df_redemption, method):
590
+ tmp = df_aggregate.loc[("Portfolio", slice(None)), (slice(None), method)].droplevel(0, axis=0)
591
+ tmp = tmp.mask(tmp < 0, df_aggregate.loc[("Equity", slice(None)), (slice(None), method)].droplevel(0, axis=0))
592
+ df_aggregate = tmp.droplevel(1, axis=1)
593
+ df_redemption = df_redemption.filter(like="net_perc").mean()
594
+ gross = (
595
+ df_redemption.filter(like="net_perc_gross")
596
+ .sort_values()
597
+ .rename(
598
+ index={
599
+ "net_perc_gross_red": "1 day or less",
600
+ "net_perc_gross_red 5D": "2-7 days",
601
+ "net_perc_gross_red 12D": "8-15 days",
602
+ "net_perc_gross_red 23D": "16-30 days",
603
+ "net_perc_gross_red 45D": "31-60 days",
604
+ "net_perc_gross_red 120D": "61-180 days",
605
+ }
606
+ )
607
+ )
608
+ net = (
609
+ df_redemption.filter(like="net_perc_net")
610
+ .sort_values()
611
+ .rename(
612
+ index={
613
+ "net_perc_net_red": "1 day or less",
614
+ "net_perc_net_red 5D": "2-7 days",
615
+ "net_perc_net_red 12D": "8-15 days",
616
+ "net_perc_net_red 23D": "16-30 days",
617
+ "net_perc_net_red 45D": "31-60 days",
618
+ "net_perc_net_red 120D": "61-180 days",
619
+ }
620
+ )
621
+ )
622
+ lcr_vs_net = df_aggregate.div(net, axis=0) * 100
623
+ lcr_vs_net = lcr_vs_net.reindex(df_aggregate.index, axis=0) # preserve index order
624
+ lcr_vs_net_colors = lcr_vs_net.applymap(
625
+ lambda x: "#0FFBA6" if x > 200 else "#FC6955" if x < 120 else "#FBE426"
626
+ )
627
+ lcr_vs_net = lcr_vs_net.applymap(lambda x: f"{x: .0f}%" if x <= 200 else ">200%")
628
+ lcr_vs_gross = df_aggregate.div(gross, axis=0) * 100
629
+ lcr_vs_gross = lcr_vs_gross.reindex(df_aggregate.index, axis=0) # preserve index order
630
+ lcr_vs_gross_colors = lcr_vs_gross.applymap(
631
+ lambda x: "#0FFBA6" if x > 200 else "#FC6955" if x < 120 else "#FBE426"
632
+ )
633
+ lcr_vs_gross = lcr_vs_gross.applymap(lambda x: f"{x: .0f}%" if x <= 200 else ">200%")
634
+
635
+ fig = make_subplots(
636
+ rows=1,
637
+ cols=2,
638
+ specs=[[{"type": "table"}, {"type": "table"}]],
639
+ subplot_titles=(
640
+ "Liquidity Coverage Ratio (vs net redemptions)",
641
+ "Liquidity Coverage Ratio (vs gross redemptions)",
642
+ ),
643
+ )
644
+ headers = df_aggregate.index.to_list()
645
+ headers.insert(0, "Scenarios")
646
+ net_cells = lcr_vs_net.values.tolist()
647
+ net_cells.insert(0, lcr_vs_net.columns.to_list())
648
+ gross_cells = lcr_vs_gross.values.tolist()
649
+ gross_cells.insert(0, lcr_vs_gross.columns.to_list())
650
+
651
+ fig.add_table(
652
+ columnwidth=[50, 40],
653
+ header=dict(
654
+ values=headers,
655
+ line_color="darkslategray",
656
+ fill_color="royalblue",
657
+ align="center",
658
+ font=dict(color="white", size=12),
659
+ height=40,
660
+ ),
661
+ cells=dict(
662
+ values=net_cells,
663
+ line_color="darkslategray",
664
+ align="center",
665
+ font=dict(color="black", size=11),
666
+ fill=dict(color=["paleturquoise"] + lcr_vs_net_colors.values.tolist()),
667
+ height=30,
668
+ ),
669
+ row=1,
670
+ col=1,
671
+ )
672
+ fig.add_table(
673
+ columnwidth=[50, 40],
674
+ header=dict(
675
+ values=headers,
676
+ line_color="darkslategray",
677
+ fill_color="royalblue",
678
+ align="center",
679
+ font=dict(color="white", size=12),
680
+ height=40,
681
+ ),
682
+ cells=dict(
683
+ values=gross_cells,
684
+ line_color="darkslategray",
685
+ align="center",
686
+ font=dict(color="black", size=11),
687
+ fill=dict(color=["paleturquoise"] + lcr_vs_gross_colors.values.tolist()),
688
+ height=30,
689
+ ),
690
+ row=1,
691
+ col=2,
692
+ )
693
+
694
+ return fig
695
+
696
+ @staticmethod
697
+ def asset_liquidity_profile_stress_testing_bar_char(df_aggregate, method):
698
+ pd.options.plotting.backend = "plotly"
699
+ df = (
700
+ (df_aggregate.loc[("Portfolio", slice(None)), (slice(None), method)] * 100)
701
+ .astype(float)
702
+ .round(2)
703
+ .droplevel(0)
704
+ .droplevel(1, axis=1)
705
+ )
706
+ df = df.drop("Baseline Scenario", axis=1)
707
+
708
+ colors = ["green", "lightgreen", "lightblue", "lightsalmon", "orange", "red"]
709
+
710
+ fig = df.diff().fillna(df).T.plot.bar()
711
+ fig.update_yaxes(ticksuffix="%", title="Percent (%)", range=[0, 105])
712
+ fig.update_traces(texttemplate="%{value}", textposition="outside")
713
+ fig.update_layout(
714
+ barmode="group",
715
+ legend=dict(orientation="h", yanchor="bottom", y=1.01, xanchor="left", font=dict(size=15), title=""),
716
+ )
717
+
718
+ for i in range(len(fig["data"])):
719
+ fig["data"][i]["marker"]["color"] = colors[i]
720
+
721
+ return fig
722
+
723
+ @staticmethod
724
+ def asset_liquidity_profile_stress_testing_table(df_aggregate, scenario1, scenario2, method):
725
+ fig = make_subplots(
726
+ rows=1,
727
+ cols=2,
728
+ specs=[[{"type": "table"}, {"type": "table"}]],
729
+ subplot_titles=(f"{scenario1}", f"{scenario2}"),
730
+ )
731
+
732
+ def add_table_to_fig(scenario, col):
733
+ df = (df_aggregate.loc[:, (scenario, method)] * 100).astype(float).round(2)
734
+ df_table = df.unstack().reindex(df.index.get_level_values(0).drop_duplicates())
735
+ df_table = df_table.reindex(df.index.get_level_values(1).drop_duplicates(), axis=1)
736
+ headers = df_table.columns.insert(0, "Asset Type")
737
+ cells_values = df_table.T.values.tolist()
738
+ cells_values.insert(0, df_table.index.to_list()) # Index in values for the table.
739
+ fig.add_table(
740
+ header=dict(
741
+ values=headers,
742
+ line_color="darkslategray",
743
+ fill_color="royalblue",
744
+ font=dict(color="white", size=12),
745
+ align="center",
746
+ height=30,
747
+ ),
748
+ cells=dict(
749
+ values=cells_values, # 2nd column
750
+ line_color="darkslategray",
751
+ fill_color=["paleturquoise", "white"],
752
+ font=dict(color="black", size=11),
753
+ suffix=[None] + ["%"],
754
+ align="center",
755
+ height=30,
756
+ ),
757
+ row=1,
758
+ col=col,
759
+ )
760
+
761
+ if scenario1:
762
+ add_table_to_fig(scenario1, 1)
763
+ if scenario2:
764
+ add_table_to_fig(scenario2, 2)
765
+ fig.update_layout(height=350)
766
+ return fig
767
+
768
+ @staticmethod
769
+ def asset_liquidity_profile_color_table():
770
+ df = pd.DataFrame(
771
+ index=["1 day is needed"] + list(map(lambda x: x + " days are needed", ["2-7", "8-15", "16-30", ">30"])),
772
+ columns=list(map(lambda x: "To liquidate " + str(x) + "% of AUM", [100, 90, 80, 70])),
773
+ )
774
+ df_colors = df.copy()
775
+ df.fillna("", inplace=True)
776
+ df_colors.iloc[0, :] = "#0FFBA6"
777
+ df_colors.loc["2-7 days are needed"] = ["#0FFBA6"] * 3 + ["#FBE426"]
778
+ df_colors.loc["8-15 days are needed"] = ["#0FFBA6"] + ["#FBE426"] * 2 + ["#FC6955"]
779
+ df_colors.loc["16-30 days are needed"] = ["#FBE426"] * 3 + ["#FC6955"]
780
+ df_colors.loc[">30 days are needed"] = ["#FBE426"] + ["#FC6955"] * 3
781
+
782
+ fig = go.Figure(
783
+ data=go.Table(
784
+ columnwidth=[3, 2],
785
+ header=dict(
786
+ values=["Colors"] + df.columns.tolist(),
787
+ line_color="darkslategray",
788
+ fill_color="white",
789
+ align="center",
790
+ font=dict(color="black", size=12),
791
+ height=30,
792
+ ),
793
+ cells=dict(
794
+ values=[df.index.tolist()] + df.T.values.tolist(),
795
+ line_color="black",
796
+ fill_color=["white"] + df_colors.T.values.tolist(),
797
+ align="center",
798
+ font=dict(color="black", size=11),
799
+ height=30,
800
+ ),
801
+ )
802
+ )
803
+ return fig
804
+
805
+ @staticmethod
806
+ def summary_ratings(series):
807
+ pass
808
+
809
+ @staticmethod
810
+ def liability_liquidity_profile_color_table():
811
+ s = pd.Series(
812
+ index=list(map(lambda x: "Redemption Coverage Ratio " + x, [">200%", "120%-200%", "<120%"])), dtype=float
813
+ )
814
+ s_colors = s.copy()
815
+ s.fillna("", inplace=True)
816
+ s_colors.iat[0] = "#0FFBA6"
817
+ s_colors.iat[1] = "#FBE426"
818
+ s_colors.iat[2] = "#FC6955"
819
+
820
+ fig = go.Figure(
821
+ data=go.Table(
822
+ header=dict(height=0),
823
+ cells=dict(
824
+ values=[s.index.tolist()] + [s.values.tolist()],
825
+ line_color="black",
826
+ fill_color=["white"] + [s_colors.values.tolist()],
827
+ align="center",
828
+ font=dict(color="black", size=11),
829
+ height=30,
830
+ ),
831
+ )
832
+ )
833
+ return fig
834
+
835
+ """ The main function for the liquidity stress tests """
836
+
837
+ def liquidity_stress_test(
838
+ self,
839
+ report_date: Optional[date] = None,
840
+ weights_date: Optional[date] = None,
841
+ liq_factor: float = 1 / 3,
842
+ below_x_days: int = 5,
843
+ ) -> dict:
844
+ if not (product_ids := self.get_product_ids_from_group_product_or_product()):
845
+ # In the case the model is not a Product or ProductGroup, we return an empty DataFrame
846
+ return dict()
847
+
848
+ # We test if a date is None, if yes, we stop the code to avoid errors.
849
+ # Weights date cannot being after report date.
850
+ # if report_date is None or weights_date > report_date:
851
+ if weights_date > report_date:
852
+ return dict()
853
+
854
+ assets = self.portfolio.assets.filter(date=weights_date)
855
+ qs_assets = assets.order_by("underlying_instrument")
856
+ if not qs_assets.exists():
857
+ return dict()
858
+
859
+ assets_fields = [
860
+ "date",
861
+ "underlying_instrument__id",
862
+ "underlying_instrument__instrument_type",
863
+ "total_value_fx_usd",
864
+ "weighting",
865
+ "shares",
866
+ ]
867
+ df_assets = (
868
+ pd.DataFrame(list(qs_assets.values_list(*assets_fields)), columns=assets_fields)
869
+ .rename(
870
+ {
871
+ "underlying_instrument__instrument_type": "type",
872
+ "underlying_instrument__id": "instrument",
873
+ "total_value_fx_usd": "total_value_usd",
874
+ },
875
+ axis=1,
876
+ )
877
+ .set_index("instrument")
878
+ .astype(dtype={"shares": "float", "total_value_usd": "float", "weighting": "float"})
879
+ )
880
+ instrument_ids = df_assets.index.unique("instrument")
881
+ start_date = date(2000, 1, 1)
882
+ qs_prices = (
883
+ InstrumentPrice.objects.filter(
884
+ calculated=False,
885
+ date__gte=start_date,
886
+ date__lte=weights_date,
887
+ instrument__in=instrument_ids,
888
+ )
889
+ .annotate_base_data()
890
+ .order_by("date", "instrument")
891
+ .select_related("currency_fx_rate_to_usd")
892
+ )
893
+ if not qs_prices.exists():
894
+ return {}
895
+
896
+ price_fields = ["date", "instrument", "net_value_usd", "volume_usd", "volume"]
897
+ df_prices = pd.DataFrame(list(qs_prices.values_list(*price_fields)), columns=price_fields)
898
+ df_prices.set_index(["date", "instrument"], inplace=True)
899
+ df_prices = df_prices.astype(float)
900
+
901
+ qs_fdm = InstrumentFDM.objects.filter(pms_instrument__in=instrument_ids)
902
+ if not qs_fdm.exists():
903
+ return {}
904
+
905
+ df_instrument_eq = pd.DataFrame(
906
+ list(qs_fdm.values_list("id", "pms_instrument")), columns=["id", "pms_instrument"]
907
+ )
908
+ df_instrument_eq.rename(columns={"id": "instrument_id"}, inplace=True)
909
+ df_ask_bid = pd.DataFrame(qs_fdm.dl.market_data(values=[MarketData.ASK, MarketData.BID], from_date=start_date))
910
+ if df_ask_bid.empty:
911
+ return {}
912
+ df_ask_bid = df_ask_bid.join(df_instrument_eq.set_index("instrument_id"), on="instrument_id")
913
+ df_ask_bid = df_ask_bid[["ask", "bid", "pms_instrument", "valuation_date", "currency"]]
914
+ df_ask_bid.rename(columns={"valuation_date": "date", "pms_instrument": "instrument"}, inplace=True)
915
+ qs_fx_rate = CurrencyFXRates.objects.filter(
916
+ currency__key__in=df_ask_bid.currency.unique(),
917
+ date__gte=date(2000, 1, 1),
918
+ date__lte=weights_date,
919
+ )
920
+ if not qs_fx_rate.exists():
921
+ return {}
922
+ currency_fields = ["date", "currency__key", "value"]
923
+ df_fx_rates = pd.DataFrame(list(qs_fx_rate.values_list(*currency_fields)), columns=currency_fields)
924
+ df_fx_rates.rename(columns={"currency__key": "currency", "value": "fx_rate"}, inplace=True)
925
+ df_fx_rates.set_index(["date", "currency"], inplace=True)
926
+ df_ask_bid = df_ask_bid.join(df_fx_rates, on=["date", "currency"]).dropna()
927
+ if df_ask_bid.empty:
928
+ return {}
929
+
930
+ df_ask_bid = df_ask_bid.set_index(["date", "instrument"]).drop("currency", axis=1).astype(float)
931
+ df_ask_bid.loc[:, ["bid", "ask"]] = df_ask_bid.loc[:, ["bid", "ask"]].div(df_ask_bid.fx_rate, axis=0)
932
+ df_ask_bid.drop(columns="fx_rate", inplace=True)
933
+
934
+ if not qs_prices.exists() or df_ask_bid.empty:
935
+ return dict()
936
+
937
+ df_prices = pd.concat([df_prices, df_ask_bid], axis=1).sort_index()
938
+ df_prices.rename(columns={"volume_usd": "dollar_volume"}, inplace=True)
939
+
940
+ df_assets.type = df_assets.type.replace("Index", "Cash")
941
+ df_assets = df_assets.set_index("type", append=True).swaplevel().sort_index()
942
+
943
+ # cleaning volumes
944
+ df_prices.dollar_volume = df_prices.dollar_volume.where(df_prices.dollar_volume > 10000)
945
+ df_prices.volume = df_prices.where(df_prices.dollar_volume.notnull()).volume
946
+
947
+ df_prices["bid_ask_spread"] = (df_prices.ask - df_prices.bid) / df_prices.ask
948
+
949
+ qs_product_price = InstrumentPrice.objects.filter(
950
+ calculated=True, date__lte=report_date, instrument__in=product_ids
951
+ ).order_by("date", "instrument")
952
+ if not qs_product_price.exists():
953
+ return dict()
954
+
955
+ product_fields = ["date", "instrument", "net_value_usd", "outstanding_shares"]
956
+ df_products_price = (
957
+ pd.DataFrame(list(qs_product_price.values(*product_fields)), columns=product_fields)
958
+ .set_index(["date", "instrument"])
959
+ .astype(float)
960
+ .groupby(level=["date", "instrument"])
961
+ .ffill()
962
+ )
963
+ df_products_price["aum"] = df_products_price.outstanding_shares * df_products_price.net_value_usd
964
+ df_aum = df_products_price.groupby(level="date").aum.sum().replace(0, method="ffill").to_frame()
965
+ from wbportfolio.models.transactions.trades import Trade
966
+
967
+ qs_trades = Trade.objects.filter(
968
+ underlying_instrument__in=product_ids,
969
+ transaction_subtype__in=["SUBSCRIPTION", "REDEMPTION"],
970
+ transaction_date__lte=report_date,
971
+ ).order_by("transaction_date")
972
+ if not qs_trades.exists():
973
+ return {}
974
+
975
+ trades_fields = [
976
+ "transaction_date",
977
+ "transaction_subtype",
978
+ "underlying_instrument",
979
+ "underlying_instrument__currency",
980
+ "total_value",
981
+ ]
982
+ df_trades = (
983
+ pd.DataFrame(list(qs_trades.values_list(*trades_fields)), columns=trades_fields)
984
+ .rename({"underlying_instrument__currency": "currency", "transaction_date": "date"}, axis=1)
985
+ .set_index(["date", "currency", "underlying_instrument"])
986
+ .astype(dtype={"total_value": "float"})
987
+ )
988
+ qs_fx_rate = CurrencyFXRates.objects.filter(
989
+ currency__in=df_trades.index.unique("currency"),
990
+ date__in=df_trades.index.unique("date"),
991
+ ).order_by("date", "currency")
992
+ fx_rates_fields = ["date", "currency", "value"]
993
+ df_fx_rates = pd.DataFrame(list(qs_fx_rate.values_list(*fx_rates_fields)), columns=fx_rates_fields)
994
+ df_fx_rates.rename(columns={"value": "fx_rate"}, inplace=True)
995
+ df_trades = df_trades.join(df_fx_rates.set_index(["date", "currency"]).astype(float))
996
+ df_trades["total_value_usd"] = df_trades.total_value / df_trades.fx_rate
997
+ df_trades = df_trades.droplevel(level="currency")
998
+ accumulated_days_list = [5, 12, 23, 45, 120]
999
+
1000
+ # df_trades.transaction_date = pd.to_datetime(df_trades["transaction_date"]) # to use df.rolling
1001
+ # Gross Redemption
1002
+ df_gross_redemption = df_trades.where(df_trades.transaction_subtype == "REDEMPTION")
1003
+ df_gross_redemption = df_gross_redemption.groupby("date").total_value_usd.sum()
1004
+ df_gross_redemption.name = "gross_redemption"
1005
+ df_gross_redemption = df_aum.join(df_gross_redemption)
1006
+ df_gross_redemption.gross_redemption = df_gross_redemption.gross_redemption.fillna(0)
1007
+ df_gross_redemption["net_perc_gross_red"] = abs(df_gross_redemption.gross_redemption) / df_gross_redemption.aum
1008
+ for acc_days in accumulated_days_list:
1009
+ df_gross_redemption[f"net_perc_gross_red {acc_days}D"] = df_gross_redemption.net_perc_gross_red.rolling(
1010
+ acc_days
1011
+ ).sum()
1012
+
1013
+ # Net Redemption
1014
+ df_trades_by_day = df_trades.groupby("date").total_value_usd.sum()
1015
+ df_net_redemption = df_trades_by_day.where(df_trades_by_day < 0)
1016
+ df_net_redemption.name = "net_redemption"
1017
+ df_net_redemption = df_aum.join(df_net_redemption)
1018
+ df_net_redemption.net_redemption = df_net_redemption.net_redemption.fillna(0)
1019
+ df_net_redemption.loc[:, "net_perc_net_red"] = abs(df_net_redemption.net_redemption) / df_net_redemption.aum
1020
+
1021
+ # Accumulated net percent redemption for both - gross and net.
1022
+ for acc_days in accumulated_days_list:
1023
+ df_net_redemption[f"net_perc_net_red {acc_days}D"] = df_net_redemption.net_perc_net_red.rolling(
1024
+ acc_days
1025
+ ).sum()
1026
+ cols_to_use = df_net_redemption.columns.difference(df_gross_redemption.columns)[::-1]
1027
+ df_redemptions = pd.concat([df_gross_redemption, df_net_redemption[cols_to_use]], axis=1)
1028
+
1029
+ expected_net_redemption = df_redemptions.filter(like="net_perc_net_red").mean().sort_values().round(4)
1030
+ gross_total_portfolio_value_usd = df_assets.total_value_usd.sum()
1031
+ net_total_portfolio_value_usd = df_redemptions.aum.values[-1] # which is for the report date
1032
+ df_assets.loc[:, "weighting"] = df_assets.loc[:, "total_value_usd"] / df_assets.loc[:, "total_value_usd"].sum()
1033
+ df_volumes = df_prices.volume.unstack("date")
1034
+ df_dollar_volume = df_prices.dollar_volume.unstack("date")
1035
+ df_bid_ask_spread = df_prices.bid_ask_spread.unstack("date")
1036
+ days_to_liquidate = self.get_volumes_from_scenario_stress_test(weights_date, df_volumes, df_assets, liq_factor)
1037
+ average_worst_dollar_volume = self.get_percentile_worst_dollar_volume(df_dollar_volume, method="other")
1038
+ rst_analysis = self.reverse_stress_test(
1039
+ gross_total_portfolio_value_usd, df_assets, average_worst_dollar_volume, liq_factor, below_x_days
1040
+ )
1041
+ stress_tests_analysis = self.stress_volume_bid_ask_test(
1042
+ df_assets, df_dollar_volume, df_volumes, df_bid_ask_spread, liq_factor
1043
+ )
1044
+ days_to_liquidate = days_to_liquidate.join(stress_tests_analysis)
1045
+ days_to_liquidate.loc[("Cash", slice(None))] = 0 # cash is instantaneous
1046
+
1047
+ tmp = df_assets.loc[:, "weighting"].to_frame(name=("instrument", "weighting")).copy()
1048
+ days_to_liquidate = days_to_liquidate.join(tmp)
1049
+ df_aggregate = self.aggregate_days_to_liquidate(days_to_liquidate)
1050
+
1051
+ portfolio_waterfall_baseline = df_aggregate.loc[
1052
+ ("Portfolio", slice(None)), ("Baseline Scenario", "waterfall")
1053
+ ].droplevel(0, axis=0)
1054
+ portfolio_slicing_baseline = df_aggregate.loc[
1055
+ ("Portfolio", slice(None)), ("Baseline Scenario", "slicing")
1056
+ ].droplevel(0, axis=0)
1057
+ portfolio_risk_waterfall = self.series_of_colors(portfolio_waterfall_baseline)
1058
+ portfolio_risk_slicing = self.series_of_colors(portfolio_slicing_baseline)
1059
+
1060
+ asset_liquidity_message = ""
1061
+ asset_liquidity_color = "#0FFBA6"
1062
+ if not portfolio_risk_waterfall.message.dropna().empty:
1063
+ asset_liquidity_message = portfolio_risk_waterfall.message.dropna().iat[0]
1064
+ asset_liquidity_color = portfolio_risk_waterfall.dropna().colors.iat[0]
1065
+ elif not portfolio_risk_slicing.message.dropna().empty:
1066
+ asset_liquidity_message = portfolio_risk_slicing.message.dropna().iat[0]
1067
+ asset_liquidity_color = portfolio_risk_slicing.dropna().colors.iat[0]
1068
+
1069
+ liability_slicing = pd.DataFrame(portfolio_slicing_baseline.values / expected_net_redemption.values)
1070
+ color_slicing = "#0FFBA6"
1071
+ if not liability_slicing.where((liability_slicing < 2) & (liability_slicing >= 1.2)).dropna().empty:
1072
+ color_slicing = "#FBE426"
1073
+ elif not liability_slicing.where(liability_slicing < 1.2).dropna().empty:
1074
+ color_slicing = "#FC6955"
1075
+ # ------------- PLOTLY GRAPHS ------------------ #
1076
+ # BASELINE SCENARIO WATERFALL FIGURE
1077
+ fig1 = self.liquidity_monitor_graph(portfolio_waterfall_baseline, expected_net_redemption)
1078
+
1079
+ fig2 = self.liquidity_monitor_graph(portfolio_slicing_baseline, expected_net_redemption)
1080
+
1081
+ fig4 = self.asset_liquidity_graph(df_aggregate, "Baseline Scenario", "waterfall")
1082
+
1083
+ fig5 = self.asset_liquidity_graph(df_aggregate, "Baseline Scenario", "slicing")
1084
+
1085
+ fig6 = self.liquidity_bucketing_graph(df_aggregate, "Baseline Scenario")
1086
+
1087
+ fig7 = self.liability_liquidity_profile_expectations_graph(df_redemptions)
1088
+
1089
+ fig8 = self.liability_liquidity_profile_metrics_graph(df_redemptions)
1090
+
1091
+ fig9 = self.liquidity_monitor_stress_testing_tables(df_aggregate, df_redemptions, "waterfall")
1092
+
1093
+ fig10 = self.liquidity_monitor_stress_testing_tables(df_aggregate, df_redemptions, "slicing")
1094
+
1095
+ fig11 = self.asset_liquidity_profile_stress_testing_bar_char(df_aggregate, "waterfall")
1096
+
1097
+ fig12 = self.asset_liquidity_profile_stress_testing_table(df_aggregate, "COVID-19", "Lehman", "waterfall")
1098
+ fig13 = self.asset_liquidity_profile_stress_testing_table(
1099
+ df_aggregate, "Lehman Spring", "Debt Crisis", "waterfall"
1100
+ )
1101
+ fig14 = self.asset_liquidity_profile_stress_testing_table(df_aggregate, "China Crisis", "Dotcom", "waterfall")
1102
+ fig15 = self.asset_liquidity_profile_stress_testing_table(
1103
+ df_aggregate, "Volume falls by 60 pct", "S-T Worst Volume", "waterfall"
1104
+ )
1105
+ fig16 = self.asset_liquidity_profile_stress_testing_table(df_aggregate, "S-T Worst B-A", None, "waterfall")
1106
+
1107
+ fig17 = self.asset_liquidity_profile_stress_testing_bar_char(df_aggregate, "slicing")
1108
+
1109
+ fig18 = self.asset_liquidity_profile_stress_testing_table(df_aggregate, "COVID-19", "Lehman", "slicing")
1110
+ fig19 = self.asset_liquidity_profile_stress_testing_table(
1111
+ df_aggregate, "Lehman Spring", "Debt Crisis", "slicing"
1112
+ )
1113
+ fig20 = self.asset_liquidity_profile_stress_testing_table(df_aggregate, "China Crisis", "Dotcom", "slicing")
1114
+ fig21 = self.asset_liquidity_profile_stress_testing_table(
1115
+ df_aggregate, "Volume falls by 60 pct", "S-T Worst Volume", "slicing"
1116
+ )
1117
+ fig22 = self.asset_liquidity_profile_stress_testing_table(df_aggregate, "S-T Worst B-A", None, "slicing")
1118
+
1119
+ # ------------- RST FIGURE -------------------------- #
1120
+
1121
+ # Create figure
1122
+ fig3 = go.Figure()
1123
+ # Add traces, one for each slider step
1124
+ for col_i, col in enumerate(rst_analysis.columns.unique(0), start=1):
1125
+ fig3.add_trace(
1126
+ go.Surface(
1127
+ visible=False,
1128
+ colorscale=[
1129
+ (0, "rgb(166,206,227)"),
1130
+ (0.1, "rgb(227,26,28)"),
1131
+ (0.6999, "rgb(251,154,153)"),
1132
+ (0.70, "rgb(247,224,45)"),
1133
+ (0.80, "rgb(247,224,45)"),
1134
+ (0.80001, "rgb(178,223,138)"),
1135
+ (1, "rgb(51,160,44)"),
1136
+ ],
1137
+ name="Aum x" + str(col_i),
1138
+ z=(rst_analysis.loc[:, (col, slice(None))] * 100).round(2).values,
1139
+ hovertemplate=(
1140
+ "<br><b>% Weights sold:: %{z}%</b><br>"
1141
+ + "<br>% Redemption: %{x}%<br>"
1142
+ + "<br>% Worst Dollar Volume: %{y}%<br>"
1143
+ ),
1144
+ )
1145
+ )
1146
+
1147
+ # Make 10th trace visible
1148
+ fig3.data[0].visible = True
1149
+
1150
+ # Create and add slider
1151
+ steps = []
1152
+ for i in range(len(fig3.data)):
1153
+ step = dict(
1154
+ method="update",
1155
+ args=[
1156
+ {"visible": [False] * len(fig3.data)},
1157
+ {
1158
+ "title": "Slider switched to AUM x "
1159
+ + str(i + 1)
1160
+ + " = $"
1161
+ + str(round((i + 1) * net_total_portfolio_value_usd / 10**6))
1162
+ + "M"
1163
+ },
1164
+ ], # layout attribute
1165
+ )
1166
+ step["args"][0]["visible"][i] = True # Toggle i'th trace to "visible"
1167
+ steps.append(step)
1168
+
1169
+ sliders = [dict(active=0, currentvalue={"prefix": "AUM: "}, pad={"t": 50}, steps=steps)]
1170
+
1171
+ fig3.update_layout(
1172
+ sliders=sliders,
1173
+ scene=dict(xaxis_title="% Redemption", yaxis_title="% Worst Dollar Volume", zaxis_title="% Weights sold"),
1174
+ )
1175
+
1176
+ # Edit slider labels
1177
+ fig3["layout"]["sliders"][0]["currentvalue"]["prefix"] = "AUM x "
1178
+ for i in range(len(rst_analysis.columns.get_level_values(0).drop_duplicates())):
1179
+ fig3["layout"]["sliders"][0]["steps"][i]["label"] = i + 1
1180
+ nb_of_holdings = len(df_assets.loc[(df_assets.index.get_level_values("type") != "Cash", slice(None))])
1181
+ _dict = {
1182
+ "report_date": report_date,
1183
+ "weights_date": weights_date,
1184
+ "name": self.portfolio.name,
1185
+ "currency": self.portfolio.currency.key,
1186
+ "nb_of_holdings": nb_of_holdings,
1187
+ "total": f"{round(net_total_portfolio_value_usd):,}".replace(",", "'"),
1188
+ "asset_liquidity_message": asset_liquidity_message,
1189
+ "asset_liquidity_color": asset_liquidity_color,
1190
+ "liability_liquidity_color": color_slicing,
1191
+ "fig1": fig1,
1192
+ "fig2": fig2,
1193
+ "fig3": fig3,
1194
+ "fig4": fig4,
1195
+ "fig5": fig5,
1196
+ "fig6": fig6,
1197
+ "fig7": fig7,
1198
+ "fig8": fig8,
1199
+ "fig9": fig9,
1200
+ "fig10": fig10,
1201
+ "fig11": fig11,
1202
+ "fig12": fig12,
1203
+ "fig13": fig13,
1204
+ "fig14": fig14,
1205
+ "fig15": fig15,
1206
+ "fig16": fig16,
1207
+ "fig17": fig17,
1208
+ "fig18": fig18,
1209
+ "fig19": fig19,
1210
+ "fig20": fig20,
1211
+ "fig21": fig21,
1212
+ "fig22": fig22,
1213
+ "fig23": self.asset_liquidity_profile_color_table(),
1214
+ "fig24": self.liability_liquidity_profile_color_table(),
1215
+ }
1216
+ return _dict
1217
+
1218
+ def pct_liquidated_below_n_days(
1219
+ self,
1220
+ evaluation_date: date,
1221
+ below_n_days: int = 5,
1222
+ liq_factor: float = 1 / 3,
1223
+ pct_worst_volume: int = 100,
1224
+ pct_redemption: int = 100,
1225
+ last_x_trading_dates: int = 60,
1226
+ is_slicing: bool = True,
1227
+ ) -> [float, None]:
1228
+ # Allows to get ids from all share classes if product is a group of product,
1229
+ # otherwise it just returns the product id.
1230
+ if not (product_ids := self.get_product_ids_from_group_product_or_product()):
1231
+ return None
1232
+
1233
+ aum = float(self.get_redemption_analysis_df(product_ids=product_ids, report_date=evaluation_date).AUM.iat[-1])
1234
+
1235
+ qs_assets = self.portfolio.assets.filter(date=evaluation_date)
1236
+
1237
+ if not qs_assets:
1238
+ return None
1239
+
1240
+ qs_assets = (
1241
+ self.portfolio.assets.filter(date=evaluation_date)
1242
+ .order_by("underlying_instrument")
1243
+ .values("underlying_instrument", "underlying_instrument__instrument_type", "weighting")
1244
+ )
1245
+
1246
+ df_assets = (
1247
+ pd.DataFrame(qs_assets)
1248
+ .rename(columns={"underlying_instrument__instrument_type": "instrument_type"})
1249
+ .set_index("underlying_instrument")
1250
+ .astype({"weighting": "float"})
1251
+ )
1252
+
1253
+ first_trading_date = (evaluation_date - BDay(last_x_trading_dates)).date()
1254
+
1255
+ qs_price = (
1256
+ InstrumentPrice.objects.filter(
1257
+ instrument__in=df_assets.index, date__gte=first_trading_date, date__lte=evaluation_date
1258
+ )
1259
+ .order_by("date", "instrument")
1260
+ .values("date", "instrument__currency", "instrument", "net_value", "volume")
1261
+ )
1262
+
1263
+ df_prices = (
1264
+ pd.DataFrame(qs_price)
1265
+ .rename(columns={"instrument__currency": "currency"})
1266
+ .set_index(["date", "currency", "instrument"])
1267
+ )
1268
+
1269
+ qs_currencies = (
1270
+ CurrencyFXRates.objects.filter(
1271
+ currency__in=df_prices.index.get_level_values("currency"),
1272
+ date__gte=first_trading_date,
1273
+ date__lte=evaluation_date,
1274
+ )
1275
+ .order_by("date", "currency__id")
1276
+ .values("date", "currency", "value")
1277
+ )
1278
+ df_currencies = pd.DataFrame(qs_currencies).set_index(["date", "currency"])
1279
+ df_prices = df_prices.join(df_currencies)
1280
+ df_prices["net_value_usd"] = df_prices.net_value / df_prices.value
1281
+ df_prices["dollar_volume"] = df_prices.net_value_usd * df_prices.volume
1282
+
1283
+ dollar_volume = (
1284
+ df_prices.dollar_volume.where(df_prices.dollar_volume > 10000).dropna().droplevel("currency").unstack()
1285
+ )
1286
+
1287
+ average_worst_dollar_volume = dollar_volume.where(
1288
+ dollar_volume.le(dollar_volume.astype(float).quantile(pct_worst_volume / 100))
1289
+ ).mean()
1290
+
1291
+ days_to_liquidate = below_n_days / (
1292
+ aum * pct_redemption / 100 * df_assets.weighting / (liq_factor * average_worst_dollar_volume)
1293
+ )
1294
+
1295
+ if is_slicing: # if False, computations are done for Waterfall already.
1296
+ days_to_liquidate.loc[
1297
+ df_assets[(df_assets.instrument_type != "Cash") & (df_assets.instrument_type != "Index")].index
1298
+ ] = days_to_liquidate.min()
1299
+
1300
+ weights_sold = days_to_liquidate.mask(days_to_liquidate > 1, 1).mul(df_assets.weighting).sum()
1301
+ weights_sold += df_assets.loc[
1302
+ df_assets[(df_assets.instrument_type.key == "cash") | (df_assets.instrument_type.key == "index")].index,
1303
+ "weighting",
1304
+ ].sum()
1305
+
1306
+ # readable_result = f"{round(weights_sold * 100, 2)}%"
1307
+ return weights_sold