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,86 @@
1
+ from typing import Generator
2
+
3
+ from django.contrib.contenttypes.models import ContentType
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 wbcore.serializers.fields.number import percent_decorator
8
+ from wbfdm.models import Instrument
9
+ from wbportfolio.pms.typing import Portfolio as PortfolioDTO
10
+
11
+ from .mixins import ActiveProductRelationshipMixin
12
+
13
+
14
+ @register("Liquidity Stress Instrument Rule Backend", rule_group_key="portfolio")
15
+ class RuleBackend(ActiveProductRelationshipMixin, backend.AbstractRuleBackend):
16
+ @classmethod
17
+ def get_parameter_fields(cls):
18
+ return [
19
+ "group_by",
20
+ "field",
21
+ "is_cash",
22
+ "asset_classes",
23
+ "currencies",
24
+ "countries",
25
+ "classification_height",
26
+ "classifications",
27
+ ]
28
+
29
+ @classmethod
30
+ def get_serializer_class(cls) -> wb_serializers.Serializer:
31
+ class RuleBackendSerializer(wb_serializers.Serializer):
32
+ below_x_days = wb_serializers.IntegerField(default=5, label="Below X days")
33
+ liquidity_factor = wb_serializers.FloatField(default=0.33, label="Liquidity factory")
34
+ pct_worst_volume = wb_serializers.FloatField(
35
+ default=100.0, label="Worst volume (%)", percent=True, decorators=[percent_decorator]
36
+ )
37
+ pct_redemption = wb_serializers.FloatField(
38
+ default=100.0, label="Redemption (%)", percent=True, decorators=[percent_decorator]
39
+ )
40
+ last_x_trading_dates = wb_serializers.IntegerField(default=60, label="Number of trading days")
41
+ is_slicing = wb_serializers.BooleanField(default=True, label="Slicing")
42
+
43
+ @classmethod
44
+ def get_parameter_fields(cls):
45
+ return [
46
+ "below_x_days",
47
+ "liquidity_factor",
48
+ "pct_worst_volume",
49
+ "pct_redemption",
50
+ "last_x_trading_dates",
51
+ "is_slicing",
52
+ ]
53
+
54
+ return RuleBackendSerializer
55
+
56
+ def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
57
+ # TODO adapt to DTO framework
58
+ factor = self.instrument.pct_liquidated_below_n_days(
59
+ self.evaluation_date,
60
+ below_n_days=self.below_x_days,
61
+ liq_factor=self.liquidity_factor,
62
+ pct_worst_volume=self.pct_worst_volume,
63
+ pct_redemption=self.pct_redemption,
64
+ last_x_trading_dates=self.last_x_trading_dates,
65
+ is_slicing=self.is_slicing,
66
+ )
67
+ for threshold in self.thresholds:
68
+ if factor is not None and threshold.is_inrange(factor):
69
+ yield backend.IncidentResult(
70
+ breached_object=self.instrument,
71
+ breached_object_repr=str(self.instrument),
72
+ breached_value=str(factor),
73
+ report_details=dict(),
74
+ severity=threshold.severity,
75
+ )
76
+
77
+ @classmethod
78
+ def get_allowed_content_type(cls) -> "ContentType":
79
+ return ContentType.objects.get_for_model(Instrument)
80
+
81
+ def is_passive_evaluation_valid(self) -> bool:
82
+ if not self.instrument.portfolio:
83
+ return False
84
+ return (self.instrument.portfolio.imported_assets.filter(date=self.evaluation_date).exists()) and (
85
+ self.instrument.valuations.filter(date=self.evaluation_date).exists()
86
+ )
@@ -0,0 +1,220 @@
1
+ from datetime import date, 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 pandas.tseries.offsets import BDay
8
+ from wbcompliance.models.risk_management import backend
9
+ from wbcore import serializers as wb_serializers
10
+ from wbfdm.backends.dto import PriceDTO
11
+ from wbfdm.models import Instrument, InstrumentPrice, InstrumentType
12
+ from wbfdm.serializers import (
13
+ InstrumentTypeRepresentationSerializer,
14
+ SecurityRepresentationSerializer,
15
+ )
16
+ from wbportfolio.models import InstrumentPortfolioThroughModel, Portfolio
17
+
18
+
19
+ class ActivePortfolioRelationshipMixin(backend.AbstractRuleBackend):
20
+ OBJECT_FIELD_NAME: str = "portfolio"
21
+
22
+ portfolio: Portfolio
23
+
24
+ def is_passive_evaluation_valid(self) -> bool:
25
+ return self.portfolio.imported_assets.filter(date=self.evaluation_date).exists()
26
+
27
+ @classmethod
28
+ def get_allowed_content_type(cls) -> "ContentType":
29
+ return ContentType.objects.get_for_model(Portfolio)
30
+
31
+ def _build_dto_args(self):
32
+ return (self.portfolio._build_dto(self.evaluation_date, is_estimated=False),)
33
+
34
+ @classmethod
35
+ def get_all_active_relationships(cls) -> models.QuerySet:
36
+ valid_relationships = InstrumentPortfolioThroughModel.objects.filter(
37
+ instrument__instrument_type__key="product"
38
+ ).values("portfolio")
39
+ return Portfolio.objects.filter(id__in=valid_relationships, is_tracked=True)
40
+
41
+
42
+ class ActiveProductRelationshipMixin(backend.AbstractRuleBackend):
43
+ OBJECT_FIELD_NAME: str = "product"
44
+
45
+ @classmethod
46
+ def get_allowed_content_type(cls) -> "ContentType":
47
+ return ContentType.objects.get_for_model(Instrument)
48
+
49
+ @classmethod
50
+ def get_all_active_relationships(cls) -> models.QuerySet:
51
+ return Instrument.active_objects.filter(instrument_type__key="product")
52
+
53
+
54
+ class StopLossMixin(backend.AbstractRuleBackend):
55
+ class FreqChoices(models.TextChoices):
56
+ BUSINESS_DAY = "B", "Business Day"
57
+ WEEKLY_FRIDAY = "W-FRI", "Friday to Friday"
58
+ BUSINESS_MONTHLY = "BME", "Business Monthly"
59
+ BUSINESS_YEARLY = "BYE", "Business Yearly"
60
+
61
+ class DateIntervalOption(models.TextChoices):
62
+ ROLLING_WINDOWS = "ROLLING_WINDOWS", "Rolling Window"
63
+ FREQUENCY = "FREQUENCY", "Frequency"
64
+
65
+ class DynamicBenchmarkType(models.TextChoices):
66
+ PORTFOLIO = "PORTFOLIO", "Primary Portfolio"
67
+ PRIMARY_BENCHMARK = "PRIMARY_BENCHMARK", "Primary Benchmark"
68
+
69
+ class FieldType(models.TextChoices):
70
+ OUTSTANDING_SHARES = "outstanding_shares", "Outstanding shares"
71
+ NET_VALUE = "net_value", "Net Value"
72
+
73
+ def __init__(self, *args, **kwargs):
74
+ super().__init__(*args, **kwargs)
75
+ self.portfolio = self.product.portfolio
76
+
77
+ @classmethod
78
+ def get_serializer_class(cls) -> wb_serializers.Serializer:
79
+ class RuleBackendSerializer(wb_serializers.Serializer):
80
+ freq = wb_serializers.ChoiceField(
81
+ choices=cls.FreqChoices,
82
+ default=cls.FreqChoices.WEEKLY_FRIDAY,
83
+ label="Frequency",
84
+ help_text="Valid only if the interval Option is Frequency. Specify the frequency use for aggregation",
85
+ )
86
+ date_interval_option = wb_serializers.ChoiceField(
87
+ choices=cls.DateIntervalOption,
88
+ default=cls.DateIntervalOption.ROLLING_WINDOWS,
89
+ label="Interval Option",
90
+ )
91
+ dynamic_benchmark_type = wb_serializers.ChoiceField(
92
+ choices=cls.DynamicBenchmarkType,
93
+ default=None,
94
+ allow_null=True,
95
+ label="Benchmark Type",
96
+ help_text="If specified, will compare the stop loss against the instrument related potential benchmark performance",
97
+ )
98
+ rolling_window_interval = wb_serializers.IntegerField(
99
+ default=7,
100
+ label="Rolling Window Interval",
101
+ help_text="Valid only if interval option is Rolling Window. Specify the number of day for sampling",
102
+ )
103
+ static_benchmark = wb_serializers.PrimaryKeyRelatedField(
104
+ queryset=Instrument.objects.all(),
105
+ default=None,
106
+ allow_null=True,
107
+ label="Static Benchmark (If any)",
108
+ help_text="If specified, will compare the stop loss against this benchmark",
109
+ )
110
+ asset_class = wb_serializers.PrimaryKeyRelatedField(
111
+ queryset=InstrumentType.objects.all(),
112
+ default=None,
113
+ allow_null=True,
114
+ label="Only Asset Class",
115
+ )
116
+ _asset_class = InstrumentTypeRepresentationSerializer(source="asset_class")
117
+
118
+ _static_benchmark = SecurityRepresentationSerializer(
119
+ source="static_benchmark", default=None, allow_null=True
120
+ )
121
+ field = wb_serializers.ChoiceField(
122
+ choices=cls.FieldType.choices,
123
+ default=cls.FieldType.NET_VALUE,
124
+ label="Field",
125
+ )
126
+ penny_stock_max_abs_net_value = wb_serializers.FloatField(
127
+ default=0.001, label="Penny Stock Max Absolute Net value"
128
+ )
129
+
130
+ @classmethod
131
+ def get_parameter_fields(cls):
132
+ return [
133
+ "freq",
134
+ "date_interval_option",
135
+ "dynamic_benchmark_type",
136
+ "rolling_window_interval",
137
+ "static_benchmark",
138
+ "asset_class",
139
+ "field",
140
+ "penny_stock_max_abs_net_value",
141
+ ]
142
+
143
+ return RuleBackendSerializer
144
+
145
+ @property
146
+ def benchmark(self):
147
+ if self.dynamic_benchmark_type == self.DynamicBenchmarkType.PRIMARY_BENCHMARK.name:
148
+ return self.product.primary_benchmark
149
+ elif self.dynamic_benchmark_type == self.DynamicBenchmarkType.PORTFOLIO.name:
150
+ return self.product
151
+ elif self.static_benchmark:
152
+ return self.static_benchmark
153
+
154
+ def is_passive_evaluation_valid(self) -> bool:
155
+ try:
156
+ last_price = self.product.get_price(self.evaluation_date)
157
+ base_condition = last_price != 0
158
+ if (benchmark := self.benchmark) and (last_benchmark_price := benchmark.get_price(self.evaluation_date)):
159
+ return (last_benchmark_price != 0) and base_condition
160
+ return base_condition
161
+ except ValueError:
162
+ return False
163
+
164
+ def _generate_incidents(
165
+ self,
166
+ tested_instrument_id: int,
167
+ perf_instrument: float,
168
+ perf_benchmark: float,
169
+ ) -> Generator[backend.IncidentResult, None, None]:
170
+ total_perf = perf_instrument if perf_benchmark is None else perf_instrument - perf_benchmark
171
+ field_label = self.FieldType(self.field).label
172
+ for threshold in self.thresholds:
173
+ if threshold.is_inrange(total_perf):
174
+ instrument = Instrument.objects.get(id=tested_instrument_id)
175
+ report_details = {
176
+ "Absolute Percentage": f"{perf_instrument:,.3%}",
177
+ "Field": field_label,
178
+ }
179
+ if self.benchmark:
180
+ report_details[f"Relative Percentage VS {str(self.benchmark)}"] = f"{total_perf:,.3%}"
181
+ if total_perf < 0:
182
+ breached_value = f'<span style="color:red">{total_perf:+,.2%}</span>'
183
+ else:
184
+ breached_value = f'<span style="color:green">{total_perf:+,.2%}</span>'
185
+ yield backend.IncidentResult(
186
+ breached_object=instrument,
187
+ breached_object_repr=str(instrument),
188
+ breached_value=breached_value,
189
+ report_details=report_details,
190
+ severity=threshold.severity,
191
+ )
192
+
193
+ def _get_start_interval(self) -> date:
194
+ if self.date_interval_option == self.DateIntervalOption.FREQUENCY.name:
195
+ return pd.date_range(end=self.evaluation_date, periods=2, freq=self.FreqChoices(self.freq).value)[0].date()
196
+ else:
197
+ return (self.evaluation_date - timedelta(days=self.rolling_window_interval - 1) - BDay(1)).date()
198
+
199
+ def _get_performance(self, valuation_dto: PriceDTO) -> float:
200
+ if not valuation_dto:
201
+ return 0.0
202
+ start_date = self._get_start_interval()
203
+ if (
204
+ qs := InstrumentPrice.objects.filter(
205
+ calculated=False,
206
+ instrument_id=valuation_dto.instrument,
207
+ date__lte=start_date,
208
+ date__gte=start_date - BDay(2), # we allow 2 business day interval in case of market holiday
209
+ ).exclude(
210
+ net_value__lte=self.penny_stock_max_abs_net_value, net_value__gte=-self.penny_stock_max_abs_net_value
211
+ )
212
+ ).exists():
213
+ last_value = float(getattr(qs.latest("date"), self.field))
214
+ return float(getattr(valuation_dto, self.field)) / last_value - 1
215
+ return 0.0
216
+
217
+ def _build_dto_args(self) -> tuple[PriceDTO, PriceDTO | None]:
218
+ if benchmark := self.benchmark:
219
+ return self.product._build_dto(self.evaluation_date), benchmark._build_dto(self.evaluation_date)
220
+ return self.product._build_dto(self.evaluation_date), None
@@ -0,0 +1,111 @@
1
+ from typing import Generator
2
+
3
+ import pandas as pd
4
+ from django.contrib.contenttypes.models import ContentType
5
+ from django.db import models
6
+ from wbcompliance.models.risk_management import backend
7
+ from wbcompliance.models.risk_management.dispatch import register
8
+ from wbcore import serializers as wb_serializers
9
+ from wbfdm.models import InstrumentPrice
10
+ from wbportfolio.models import AssetPosition, Product
11
+
12
+
13
+ @register("Product Data Integrity", rule_group_key="portfolio")
14
+ class RuleBackend(backend.AbstractRuleBackend):
15
+ OBJECT_FIELD_NAME: str = "product"
16
+
17
+ class DataTypeChoices(models.TextChoices):
18
+ INSTRUMENT_PRICE = "INSTRUMENT_PRICE", "Valuation"
19
+ ASSET_POSITION = "ASSET_POSITION", "Asset Position"
20
+
21
+ product: Product
22
+
23
+ def is_passive_evaluation_valid(self) -> bool:
24
+ return self.product.is_active_at_date(self.evaluation_date)
25
+
26
+ @classmethod
27
+ def get_allowed_content_type(cls) -> "ContentType":
28
+ return ContentType.objects.get_for_model(Product)
29
+
30
+ def _build_dto_args(self):
31
+ return (self.product,)
32
+
33
+ @classmethod
34
+ def get_all_active_relationships(cls) -> models.QuerySet:
35
+ return Product.active_objects.all()
36
+
37
+ @classmethod
38
+ def get_serializer_class(cls) -> wb_serializers.Serializer:
39
+ class RuleBackendSerializer(wb_serializers.Serializer):
40
+ data_type = wb_serializers.MultipleChoiceField(
41
+ choices=cls.DataTypeChoices.choices,
42
+ default=[cls.DataTypeChoices.ASSET_POSITION.value, cls.DataTypeChoices.INSTRUMENT_PRICE.value],
43
+ label="Flags",
44
+ help_text="Set the flags that will trigger the rule",
45
+ )
46
+
47
+ @classmethod
48
+ def get_parameter_fields(cls):
49
+ return ["data_type"]
50
+
51
+ return RuleBackendSerializer
52
+
53
+ def _process_dto(self, product: Product, **kwargs) -> Generator[backend.IncidentResult, None, None]:
54
+ for lag_threshold in self.thresholds:
55
+ numerical_range = lag_threshold.numerical_range
56
+
57
+ last_asset_position_date = (
58
+ AssetPosition.objects.filter(is_estimated=False, portfolio=product.portfolio).latest("date").date
59
+ )
60
+ last_instrument_price_date = (
61
+ InstrumentPrice.objects.filter(calculated=False, instrument=product).latest("date").date
62
+ )
63
+
64
+ asset_position_lag = (
65
+ len(
66
+ pd.date_range(
67
+ min([self.evaluation_date, last_asset_position_date]), self.evaluation_date, freq="B"
68
+ )
69
+ )
70
+ - 1
71
+ )
72
+ instrument_price_lag = (
73
+ len(
74
+ pd.date_range(
75
+ min([self.evaluation_date, last_instrument_price_date]), self.evaluation_date, freq="B"
76
+ )
77
+ )
78
+ - 1
79
+ )
80
+
81
+ if (
82
+ self.DataTypeChoices.ASSET_POSITION.value in self.data_type
83
+ and asset_position_lag >= numerical_range[0]
84
+ and asset_position_lag < numerical_range[1]
85
+ ):
86
+ yield backend.IncidentResult(
87
+ breached_object=product,
88
+ breached_object_repr=str(product),
89
+ breached_value=str(asset_position_lag),
90
+ report_details={
91
+ "Last Datapoint": f"{last_asset_position_date:%d.%m.%Y}",
92
+ "Data Type": "Asset Position",
93
+ },
94
+ severity=lag_threshold.severity,
95
+ )
96
+ if (
97
+ self.DataTypeChoices.INSTRUMENT_PRICE.value in self.data_type
98
+ and instrument_price_lag >= numerical_range[0]
99
+ and instrument_price_lag < numerical_range[1]
100
+ ):
101
+ yield backend.IncidentResult(
102
+ breached_object=product,
103
+ breached_object_repr=str(product),
104
+ breached_value=str(instrument_price_lag),
105
+ report_details={
106
+ "Lag": instrument_price_lag,
107
+ "Last Datapoint": f"{last_instrument_price_date:%d.%m.%Y}",
108
+ "Data Type": "Valuation",
109
+ },
110
+ severity=lag_threshold.severity,
111
+ )
@@ -0,0 +1,24 @@
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 wbportfolio.pms.typing import Valuation as ValuationDTO
6
+
7
+ from .mixins import ActiveProductRelationshipMixin, StopLossMixin
8
+
9
+
10
+ @register("Stop Loss Instrument Rule Backend", rule_group_key="portfolio")
11
+ class RuleBackend(ActiveProductRelationshipMixin, StopLossMixin, backend.AbstractRuleBackend):
12
+ def is_passive_evaluation_valid(self) -> bool:
13
+ return (
14
+ super().is_passive_evaluation_valid()
15
+ and self.product
16
+ and self.product.valuations.filter(date=self.evaluation_date).exists()
17
+ )
18
+
19
+ def _process_dto(
20
+ self, instrument_valuation_dto: ValuationDTO, benchmark_valuation_dto: ValuationDTO = None, *args, **kwargs
21
+ ) -> Generator[backend.IncidentResult, None, None]:
22
+ perf_instrument = self._get_performance(instrument_valuation_dto)
23
+ perf_benchmark = self._get_performance(benchmark_valuation_dto)
24
+ yield from self._generate_incidents(self.product.id, perf_instrument, perf_benchmark)
@@ -0,0 +1,36 @@
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 wbportfolio.pms.typing import Portfolio as PortfolioDTO
6
+ from wbportfolio.pms.typing import Valuation as ValuationDTO
7
+
8
+ from .mixins import ActiveProductRelationshipMixin, StopLossMixin
9
+
10
+
11
+ @register("Stop Loss Portfolio Rule Backend", rule_group_key="portfolio")
12
+ class RuleBackend(ActiveProductRelationshipMixin, StopLossMixin, backend.AbstractRuleBackend):
13
+ def _build_dto_args(self) -> tuple[PortfolioDTO, ValuationDTO | None]:
14
+ return (
15
+ self.portfolio._build_dto(self.evaluation_date),
16
+ super()._build_dto_args()[1],
17
+ )
18
+
19
+ def is_passive_evaluation_valid(self) -> bool:
20
+ return (
21
+ super().is_passive_evaluation_valid()
22
+ and self.portfolio
23
+ and self.portfolio.assets.filter(date=self.evaluation_date).exists()
24
+ )
25
+
26
+ def _process_dto(
27
+ self, portfolio: PortfolioDTO, benchmark_valuation_dto: ValuationDTO = None, **kwargs
28
+ ) -> Generator[backend.IncidentResult, None, None]:
29
+ if self.asset_class:
30
+ portfolio = PortfolioDTO(filter(lambda x: x.instrument_type == self.asset_class.id, portfolio.positions))
31
+ perf_benchmark = self._get_performance(benchmark_valuation_dto)
32
+ for instrument_id, pos in portfolio.positions_map.items():
33
+ if pos.price is not None:
34
+ perf_instrument = self._get_performance(ValuationDTO(instrument=instrument_id, net_value=pos.price))
35
+
36
+ yield from self._generate_incidents(instrument_id, perf_instrument, perf_benchmark)
@@ -0,0 +1,63 @@
1
+ from typing import Generator
2
+
3
+ import pandas as pd
4
+ from wbcompliance.models import RiskIncidentType
5
+ from wbcompliance.models.risk_management import backend
6
+ from wbcompliance.models.risk_management.dispatch import register
7
+ from wbcore import serializers as wb_serializers
8
+ from wbcore.serializers.fields.number import percent_decorator
9
+ from wbfdm.models import Instrument
10
+ from wbportfolio.pms.typing import Portfolio as PortfolioDTO
11
+
12
+ from .mixins import ActivePortfolioRelationshipMixin
13
+
14
+
15
+ @register("UCITS 5|10|40 Portfolio Rule Backend", rule_group_key="portfolio")
16
+ class RuleBackend(ActivePortfolioRelationshipMixin, backend.AbstractRuleBackend):
17
+ DEFAULT_THRESHOLD_1: float = 0.05
18
+ DEFAULT_THRESHOLD_2: float = 0.10
19
+ DEFAULT_THRESHOLD_3: float = 0.40
20
+
21
+ @classmethod
22
+ def get_serializer_class(cls) -> wb_serializers.Serializer:
23
+ class RuleBackendSerializer(wb_serializers.Serializer):
24
+ threshold_1 = wb_serializers.FloatField(
25
+ default=cls.DEFAULT_THRESHOLD_1, label="Threshold 1", percent=True, decorators=[percent_decorator]
26
+ )
27
+ threshold_2 = wb_serializers.FloatField(
28
+ default=cls.DEFAULT_THRESHOLD_2, label="Threshold 2", percent=True, decorators=[percent_decorator]
29
+ )
30
+ threshold_3 = wb_serializers.FloatField(
31
+ default=cls.DEFAULT_THRESHOLD_3, label="Threshold 3", percent=True, decorators=[percent_decorator]
32
+ )
33
+
34
+ @classmethod
35
+ def get_parameter_fields(cls):
36
+ return ["threshold_1", "threshold_2", "threshold_3"]
37
+
38
+ return RuleBackendSerializer
39
+
40
+ def _filter_df(self, df):
41
+ if df.empty:
42
+ return df
43
+ return df[(df["weighting"] >= self.threshold_1) & (~df["is_cash"])]
44
+
45
+ def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
46
+ if not (df := self._filter_df(pd.DataFrame(portfolio.to_df()).astype({"weighting": float}))).empty:
47
+ df = df[["underlying_instrument", "weighting"]].groupby("underlying_instrument").sum()
48
+ total_weight_threshold_1_2 = df.loc[df["weighting"] < self.threshold_2, "weighting"].sum()
49
+ highest_incident_type = RiskIncidentType.objects.order_by("-severity_order").first()
50
+ for id, row in df.to_dict("index").items():
51
+ if (row["weighting"] > self.threshold_2) or (total_weight_threshold_1_2 > self.threshold_3):
52
+ instrument = Instrument.objects.get(id=id)
53
+ yield backend.IncidentResult(
54
+ breached_object=instrument,
55
+ breached_object_repr=str(instrument),
56
+ breached_value=f'∑[0%, {self.threshold_2:.2%}]: {row["weighting"]:+.2%} | ∑[{self.threshold_1:.2%}, {self.threshold_2:.2%}]: {total_weight_threshold_1_2:+.2%}',
57
+ report_details={
58
+ "Breach Thresholds": f"{self.threshold_1}|{self.threshold_2}|{self.threshold_3}",
59
+ "Weighting": f"{row['weighting']:.3f}",
60
+ f"Sum of positions whose weight are between {self.threshold_1:.2%} and {self.threshold_2:.2%}": f"{total_weight_threshold_1_2:.3f}",
61
+ },
62
+ severity=highest_incident_type,
63
+ )
File without changes
@@ -0,0 +1,15 @@
1
+ from pytest_factoryboy import register
2
+ from wbcompliance.factories.risk_management import (
3
+ RiskIncidentTypeFactory,
4
+ RiskRuleFactory,
5
+ RuleBackendFactory,
6
+ RuleThresholdFactory,
7
+ )
8
+ from wbfdm.factories import ControversyFactory
9
+ from wbportfolio.tests.conftest import * # noqa
10
+
11
+ register(RiskIncidentTypeFactory)
12
+ register(RiskRuleFactory)
13
+ register(RuleBackendFactory)
14
+ register(RuleThresholdFactory)
15
+ register(ControversyFactory)
@@ -0,0 +1,98 @@
1
+ import pytest
2
+ from faker import Faker
3
+ from pandas.tseries.offsets import BDay
4
+ from psycopg.types.range import NumericRange
5
+ from wbcompliance.factories.risk_management import RuleThresholdFactory
6
+ from wbportfolio.models.transactions.claim import ClaimGroupbyChoice
7
+ from wbportfolio.risk_management.backends.accounts import (
8
+ RuleBackend as AccountRuleBackend,
9
+ )
10
+
11
+ fake = Faker()
12
+
13
+
14
+ @pytest.mark.django_db
15
+ class TestAccountRuleModel:
16
+ @pytest.fixture
17
+ def account_backend(
18
+ self,
19
+ entry,
20
+ weekday,
21
+ business_days_interval,
22
+ group_by=ClaimGroupbyChoice.ACCOUNT.name,
23
+ extra_parameters=None,
24
+ ):
25
+ parameters = {"group_by": group_by, "business_days_interval": business_days_interval, "field": "SHARES"}
26
+ if extra_parameters:
27
+ parameters.update(extra_parameters)
28
+ return AccountRuleBackend(
29
+ weekday,
30
+ entry,
31
+ parameters,
32
+ [RuleThresholdFactory.create(range=NumericRange(lower=None, upper=-0.2))], # detect any -20% perf
33
+ )
34
+
35
+ @pytest.mark.parametrize(
36
+ "business_days_interval",
37
+ [7],
38
+ )
39
+ def test_check_rule_groupby_account(
40
+ self,
41
+ weekday,
42
+ entry,
43
+ business_days_interval,
44
+ product,
45
+ claim_factory,
46
+ customer_trade_factory,
47
+ account_factory,
48
+ account_backend,
49
+ ):
50
+ # Simple test to test if a valid drop in performance outside the rule window will not be detected but a one within will.
51
+ account = account_factory.create(owner=entry)
52
+ other_account = account_factory.create()
53
+ claim_factory.create(
54
+ date=(weekday - BDay(business_days_interval + 2)).date(),
55
+ account=account,
56
+ trade=customer_trade_factory.create(underlying_instrument=product),
57
+ status="APPROVED",
58
+ shares=100,
59
+ )
60
+ claim_factory.create(
61
+ date=(weekday - BDay(business_days_interval + 1)).date(),
62
+ account=account,
63
+ trade=customer_trade_factory.create(underlying_instrument=product),
64
+ status="APPROVED",
65
+ shares=-50,
66
+ ) # this drop should not be detected
67
+
68
+ claim_factory.create(
69
+ date=(weekday - BDay(business_days_interval)).date(),
70
+ account=account,
71
+ trade=customer_trade_factory.create(underlying_instrument=product),
72
+ status="APPROVED",
73
+ shares=150,
74
+ )
75
+ claim_factory.create(
76
+ date=weekday,
77
+ account=account,
78
+ trade=customer_trade_factory.create(underlying_instrument=product),
79
+ status="APPROVED",
80
+ shares=-50,
81
+ ) # this drop should be detected
82
+
83
+ claim_factory.create(
84
+ date=(weekday - BDay(business_days_interval)).date(),
85
+ account=other_account,
86
+ trade=customer_trade_factory.create(underlying_instrument=product),
87
+ status="APPROVED",
88
+ shares=150,
89
+ )
90
+ claim_factory.create(
91
+ date=weekday,
92
+ account=other_account,
93
+ trade=customer_trade_factory.create(underlying_instrument=product),
94
+ status="APPROVED",
95
+ shares=-50,
96
+ ) # this drop is valid but an another account so won't be detected
97
+ res = list(account_backend.check_rule())
98
+ assert len(res) == 1