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,98 @@
1
+ from datetime import date, timedelta
2
+
3
+ from psycopg.types.range import DateRange
4
+ from wbcore import filters as wb_filters
5
+ from wbportfolio.models import Fees, Portfolio, Trade, Transaction
6
+
7
+
8
+ def get_transaction_gte_default(field, request, view):
9
+ filter_date = date.today() - timedelta(days=90)
10
+ qs = Transaction.objects.none()
11
+ if "instrument_id" in view.kwargs:
12
+ qs = Transaction.objects.filter(underlying_instrument__id=view.kwargs["instrument_id"])
13
+ elif "portfolio_id" in view.kwargs:
14
+ qs = Transaction.objects.filter(portfolio__id=view.kwargs["portfolio_id"])
15
+ if qs.exists():
16
+ filter_date = qs.earliest("transaction_date").transaction_date
17
+ return filter_date
18
+
19
+
20
+ def get_transaction_underlying_type_choices(*args):
21
+ models = [Fees, Trade]
22
+ choices = []
23
+ for model in models:
24
+ for choice in model.Type.choices:
25
+ choices.append(choice)
26
+ return choices
27
+
28
+
29
+ def get_transaction_lte_default(field, request, view):
30
+ filter_date = date.today() + timedelta(days=7)
31
+ qs = Transaction.objects.none()
32
+ if "instrument_id" in view.kwargs:
33
+ qs = Transaction.objects.filter(underlying_instrument__id=view.kwargs["instrument_id"])
34
+ elif "portfolio_id" in view.kwargs:
35
+ qs = Transaction.objects.filter(portfolio__id=view.kwargs["portfolio_id"])
36
+ if qs.exists():
37
+ filter_date = qs.latest("transaction_date").transaction_date
38
+ return filter_date
39
+
40
+
41
+ def get_transaction_default_date_range(*args, **kwargs):
42
+ return DateRange(get_transaction_gte_default(*args, **kwargs), get_transaction_lte_default(*args, **kwargs))
43
+
44
+
45
+ class TransactionFilterSet(wb_filters.FilterSet):
46
+ transaction_date = wb_filters.DateRangeFilter(
47
+ method=wb_filters.DateRangeFilter.base_date_range_filter_method,
48
+ label="Date Range",
49
+ default=get_transaction_default_date_range,
50
+ )
51
+
52
+ portfolio = wb_filters.ModelChoiceFilter(
53
+ label="Portfolio",
54
+ queryset=Portfolio.objects.all(),
55
+ endpoint=Portfolio.get_representation_endpoint(),
56
+ value_key=Portfolio.get_representation_value_key(),
57
+ label_key=Portfolio.get_representation_label_key(),
58
+ )
59
+ total_value_usd__gte = wb_filters.NumberFilter(
60
+ lookup_expr="gte", field_name="total_value_usd", label="Total Value ($)"
61
+ )
62
+ total_value_usd__lte = wb_filters.NumberFilter(
63
+ lookup_expr="lte",
64
+ label="Total Value ($)",
65
+ field_name="total_value_usd",
66
+ )
67
+
68
+ transaction_underlying_type = wb_filters.ChoiceFilter(
69
+ label="Underlying Type",
70
+ choices=get_transaction_underlying_type_choices(),
71
+ method="filter_transaction_underlying_type",
72
+ )
73
+
74
+ def filter_transaction_underlying_type(self, queryset, name, value):
75
+ if value:
76
+ queryset = queryset.filter(transaction_underlying_type=value)
77
+ return queryset
78
+
79
+ class Meta:
80
+ model = Transaction
81
+ fields = {
82
+ "transaction_type": ["exact"],
83
+ "underlying_instrument": ["exact"],
84
+ "currency": ["exact"],
85
+ "total_value": ["gte", "exact", "lte"],
86
+ "total_value_fx_portfolio": ["gte", "exact", "lte"],
87
+ }
88
+
89
+
90
+ class TransactionPortfolioFilterSet(TransactionFilterSet):
91
+ portfolio = transaction_date__gte = transaction_date__lte = None
92
+ transaction_date = wb_filters.DateFilter(
93
+ label="Transaction Date",
94
+ lookup_expr="exact",
95
+ field_name="transaction_date",
96
+ default=get_transaction_lte_default,
97
+ required=True,
98
+ )
File without changes
@@ -0,0 +1,2 @@
1
+ from .ubs import *
2
+ from .wbfdm import *
@@ -0,0 +1,3 @@
1
+ from .asset_position import *
2
+ from .fees import *
3
+ from .instrument_price import *
@@ -0,0 +1,45 @@
1
+ import json
2
+ from datetime import date, datetime
3
+ from io import BytesIO
4
+ from typing import Optional
5
+
6
+ from django.db import models
7
+ from pandas.tseries.offsets import BDay
8
+ from wbcore.contrib.io.backends import AbstractDataBackend, register
9
+
10
+ from ..utils import process_request
11
+ from .mixin import DataBackendMixin
12
+
13
+
14
+ @register("Asset Position", provider_key="ubs", save_data_in_import_source=True, passive_only=True)
15
+ class DataBackend(DataBackendMixin, AbstractDataBackend):
16
+ def __init__(
17
+ self, import_credential: Optional[models.Model] = None, ubs_bank: Optional[models.Model] = None, **kwargs
18
+ ):
19
+ if not ubs_bank:
20
+ raise ValueError("The ubs company objects needs to be passed to this backend")
21
+ self.ubs_bank = ubs_bank
22
+ if not import_credential or not import_credential.authentication_token:
23
+ raise ValueError("UBS backend needs a valid import credential object")
24
+ self.authentication_token = import_credential.authentication_token
25
+
26
+ def get_files(
27
+ self,
28
+ execution_time: datetime,
29
+ start: date = None,
30
+ obj_external_ids: list[str] = None,
31
+ **kwargs,
32
+ ) -> BytesIO:
33
+ execution_date = (execution_time - BDay(1)).date()
34
+
35
+ endpoint = "https://neo.ubs.com/api/ged-amc/external/report/v1/valuation/{0}/{1}"
36
+ if obj_external_ids:
37
+ for external_id in obj_external_ids:
38
+ res_json = process_request(
39
+ self.authentication_token, endpoint.format(external_id, execution_date.strftime("%Y-%m-%d"))
40
+ )
41
+ if res_json:
42
+ content_file = BytesIO()
43
+ content_file.write(json.dumps(res_json).encode())
44
+ file_name = f"ubs_positions_{external_id}_{execution_date:%Y-%m-%d}_{datetime.timestamp(execution_time)}.json"
45
+ yield file_name, content_file
@@ -0,0 +1,63 @@
1
+ import json
2
+ from datetime import datetime
3
+ from io import BytesIO
4
+ from typing import Optional
5
+
6
+ from django.db import models
7
+ from dynamic_preferences.registries import global_preferences_registry
8
+ from wbcore.contrib.io.backends import AbstractDataBackend, register
9
+
10
+ from ..utils import process_request
11
+ from .mixin import DataBackendMixin
12
+
13
+
14
+ @register("Fees", provider_key="ubs", save_data_in_import_source=True, passive_only=True)
15
+ class DataBackend(DataBackendMixin, AbstractDataBackend):
16
+ def __init__(
17
+ self, import_credential: Optional[models.Model] = None, ubs_bank: Optional[models.Model] = None, **kwargs
18
+ ):
19
+ if not ubs_bank:
20
+ raise ValueError("The ubs company objects needs to be passed to this backend")
21
+ self.ubs_bank = ubs_bank
22
+ if not import_credential or not import_credential.authentication_token:
23
+ raise ValueError("UBS backend needs a valid import credential object")
24
+ self.authentication_token = import_credential.authentication_token
25
+
26
+ def get_files(
27
+ self,
28
+ execution_time: datetime,
29
+ obj_external_ids: list[str] = None,
30
+ **kwargs,
31
+ ) -> BytesIO:
32
+ execution_date = execution_time.date()
33
+
34
+ mngt_fees_endpoint = "https://neo.ubs.com/api/ged-amc/external/fee/v1/management/{0}"
35
+ perf_fees_endpoint = "https://neo.ubs.com/api/ged-amc/external/fee/v1/performance/{0}"
36
+ if obj_external_ids:
37
+ for external_id in obj_external_ids:
38
+ start = kwargs.get("start", None)
39
+ if not start:
40
+ start = global_preferences_registry.manager()["wbfdm__default_start_date_historical_import"]
41
+ mngt_res = process_request(
42
+ self.authentication_token,
43
+ mngt_fees_endpoint.format(external_id),
44
+ {"fromDate": start.strftime("%Y-%m-%d"), "toDate": execution_date.strftime("%Y-%m-%d")},
45
+ )
46
+ perf_res = process_request(
47
+ self.authentication_token,
48
+ perf_fees_endpoint.format(external_id),
49
+ {"fromDate": start.strftime("%Y-%m-%d"), "toDate": execution_date.strftime("%Y-%m-%d")},
50
+ )
51
+
52
+ if mngt_res or perf_res:
53
+ res_json = {
54
+ "performance_fees": perf_res.get("fees", []),
55
+ "management_fees": mngt_res.get("fees", []),
56
+ "isin": external_id,
57
+ }
58
+
59
+ if res_json:
60
+ content_file = BytesIO()
61
+ content_file.write(json.dumps(res_json).encode())
62
+ file_name = f"ubs_fees_{external_id}_{start:%Y-%m-%d}_{execution_date:%Y-%m-%d}_{datetime.timestamp(execution_time)}.json"
63
+ yield file_name, content_file
@@ -0,0 +1,44 @@
1
+ import json
2
+ from datetime import datetime
3
+ from io import BytesIO
4
+ from typing import Optional
5
+
6
+ from django.db import models
7
+ from pandas.tseries.offsets import BDay
8
+ from wbcore.contrib.io.backends import AbstractDataBackend, register
9
+
10
+ from ..utils import process_request
11
+ from .mixin import DataBackendMixin
12
+
13
+
14
+ @register("Instrument Prices", provider_key="ubs", save_data_in_import_source=True, passive_only=True)
15
+ class DataBackend(DataBackendMixin, AbstractDataBackend):
16
+ def __init__(
17
+ self, import_credential: Optional[models.Model] = None, ubs_bank: Optional[models.Model] = None, **kwargs
18
+ ):
19
+ if not ubs_bank:
20
+ raise ValueError("The ubs company objects needs to be passed to this backend")
21
+ self.ubs_bank = ubs_bank
22
+ if not import_credential or not import_credential.authentication_token:
23
+ raise ValueError("UBS backend needs a valid import credential object")
24
+ self.authentication_token = import_credential.authentication_token
25
+
26
+ def get_files(
27
+ self,
28
+ execution_time: datetime,
29
+ obj_external_ids: list[str] = None,
30
+ **kwargs,
31
+ ) -> BytesIO:
32
+ execution_date = (execution_time - BDay(1)).date()
33
+ endpoint = "https://neo.ubs.com/api/ged-amc/external/report/v1/valuation/{0}/{1}"
34
+ if obj_external_ids:
35
+ for external_id in obj_external_ids:
36
+ res_json = process_request(
37
+ self.authentication_token, endpoint.format(external_id, execution_date.strftime("%Y-%m-%d"))
38
+ )
39
+ res_json.pop("constituents", None)
40
+ if res_json:
41
+ content_file = BytesIO()
42
+ content_file.write(json.dumps(res_json).encode())
43
+ file_name = f"ubs_instrument_price_{external_id}_{execution_date:%Y-%m-%d}_{int(datetime.timestamp(execution_time))}.json"
44
+ yield file_name, content_file
@@ -0,0 +1,15 @@
1
+ from datetime import date
2
+
3
+ from django.db import models
4
+ from wbportfolio.models.products import Product
5
+
6
+
7
+ class DataBackendMixin:
8
+ def is_object_valid(self, obj: models.Model) -> bool:
9
+ return super().is_object_valid(obj) and obj.is_active_at_date(date.today()) and obj.isin
10
+
11
+ def get_default_queryset(self):
12
+ return Product.objects.filter(bank=self.ubs_bank, isin__isnull=False)
13
+
14
+ def get_provider_id(self, obj: models.Model) -> str:
15
+ return obj.isin
@@ -0,0 +1,58 @@
1
+ from contextlib import suppress
2
+
3
+ import pandas as pd
4
+ import requests
5
+ from django.db.models import Q
6
+ from dynamic_preferences.registries import global_preferences_registry
7
+ from wbfdm.models import Instrument
8
+
9
+
10
+ def get_timedelta_import_instrument_price():
11
+ return global_preferences_registry.manager()["wbportfolio__timedelta_import_instrument_price"]
12
+
13
+
14
+ def process_request(authentication_token: str, endpoint: str | None = None, kwargs={}) -> pd.DataFrame:
15
+ headers = {"Authorization": authentication_token}
16
+ r = requests.get(endpoint, params=kwargs, headers=headers)
17
+ if r.status_code == requests.codes.ok:
18
+ with suppress(
19
+ requests.exceptions.JSONDecodeError
20
+ ): # we catch any json decode error because the UBS api doesn't seem to respect HTTP status code rule (i.e. returns 200 even though the http content is malformed)
21
+ r_json = r.json()
22
+ if r_json.get("status", "") == "SUCCESS":
23
+ return r_json
24
+ raise ValueError(f"Issue while processing request: {r.content}")
25
+
26
+
27
+ def filter_active_instruments(_date, queryset=None):
28
+ if not queryset:
29
+ queryset = Instrument.objects
30
+ queryset = queryset.filter(Q(delisted_date__isnull=True) | Q(delisted_date__gte=_date))
31
+ queryset = queryset.filter(
32
+ Q(refinitiv_mnemonic_code__isnull=False) | Q(refinitiv_identifier_code__isnull=False) | Q(isin__isnull=False)
33
+ )
34
+ return queryset.distinct()
35
+
36
+
37
+ def chunked_queryset(queryset, chunk_size):
38
+ """Slice a queryset into chunks."""
39
+
40
+ start_pk = 0
41
+ queryset = queryset.order_by("pk")
42
+
43
+ while True:
44
+ # No entry left
45
+ if not queryset.filter(pk__gt=start_pk).exists():
46
+ break
47
+
48
+ try:
49
+ # Fetch chunk_size entries if possible
50
+ end_pk = queryset.filter(pk__gt=start_pk).values_list("pk", flat=True)[chunk_size - 1]
51
+
52
+ # Fetch rest entries if less than chunk_size left
53
+ except IndexError:
54
+ end_pk = queryset.values_list("pk", flat=True).last()
55
+
56
+ yield queryset.filter(pk__gt=start_pk).filter(pk__lte=end_pk)
57
+
58
+ start_pk = end_pk
@@ -0,0 +1,2 @@
1
+ from .adjustment import *
2
+ from .dividend import *
@@ -0,0 +1,50 @@
1
+ import json
2
+ from datetime import datetime
3
+ from io import BytesIO
4
+
5
+ from django.core.serializers.json import DjangoJSONEncoder
6
+ from pandas.tseries.offsets import BDay
7
+ from wbcore.contrib.io.backends import AbstractDataBackend, register
8
+ from wbfdm.models import Instrument
9
+ from wbportfolio.import_export.backends.utils import (
10
+ get_timedelta_import_instrument_price,
11
+ )
12
+
13
+ from .mixin import DataBackendMixin
14
+
15
+
16
+ @register("Adjustment", provider_key="wbfdm", save_data_in_import_source=False, passive_only=False)
17
+ class DataBackend(DataBackendMixin, AbstractDataBackend):
18
+ DATE_LABEL = "adjustment_date"
19
+ DEFAULT_MAPPING = {
20
+ "adjustment_date": "date",
21
+ "adjustment_factor": "factor",
22
+ "cumulative_adjustment_factor": "cumulative_factor",
23
+ "instrument_id": "instrument",
24
+ }
25
+ NONE_NULLABLE_FIELDS = list(DEFAULT_MAPPING.values())
26
+
27
+ def get_default_queryset(self):
28
+ return Instrument.objects.filter(is_investable_universe=True)
29
+
30
+ def get_files(
31
+ self,
32
+ execution_time: datetime,
33
+ **kwargs,
34
+ ) -> BytesIO:
35
+ execution_date = execution_time.date()
36
+ start = kwargs.get("start", (execution_date - BDay(get_timedelta_import_instrument_price())).date())
37
+ data = []
38
+ for dict_dto in self.get_default_queryset().dl.adjustments(
39
+ from_date=start,
40
+ to_date=execution_date,
41
+ ):
42
+ if dict_dto["adjustment_factor"] is not None:
43
+ data.append(dict_dto)
44
+ if data:
45
+ content_file = BytesIO()
46
+ content_file.write(json.dumps(data, cls=DjangoJSONEncoder).encode())
47
+ file_name = (
48
+ f"adjustment_{start:%Y-%m-%d}-{execution_date:%Y-%m-%d}_{datetime.timestamp(execution_time)}.json"
49
+ )
50
+ yield file_name, content_file
@@ -0,0 +1,16 @@
1
+ from wbcore.contrib.io.backends import AbstractDataBackend, register
2
+
3
+ from .mixin import DataBackendMixin
4
+
5
+
6
+ @register("Dividend", provider_key="wbfdm", save_data_in_import_source=False, passive_only=False)
7
+ class DataBackend(DataBackendMixin, AbstractDataBackend):
8
+ TIMEDELTA = 1
9
+ ATTRIBUTE_NAME = "dividends"
10
+ FILE_NAME = "dividend"
11
+ DEFAULT_MAPPING = {
12
+ "instrument_id": "instrument",
13
+ "rate": "weighting",
14
+ "ex_dividend_date": "value_date",
15
+ "payment_date": "transaction_date",
16
+ }
@@ -0,0 +1,15 @@
1
+ from datetime import date
2
+
3
+ from django.db import models
4
+
5
+ MUTUAL_FUND_TIMEDELTA_DAY_SHIFT = 7
6
+
7
+
8
+ class DataBackendMixin:
9
+ DATE_LABEL = "valuation_date"
10
+
11
+ def is_object_valid(self, obj: models.Model) -> bool:
12
+ return super().is_object_valid(obj) and obj.assets.exists() and obj.is_active_at_date(date.today())
13
+
14
+ def get_provider_id(self, obj: models.Model) -> str:
15
+ return obj.id
File without changes
@@ -0,0 +1,39 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ from wbcore.contrib.io.exceptions import DeserializationError
4
+ from wbcore.contrib.io.imports import ImportExportHandler
5
+ from wbfdm.import_export.handlers.instrument import InstrumentImportHandler
6
+
7
+
8
+ class AdjustmentImportHandler(ImportExportHandler):
9
+ MODEL_APP_LABEL = "wbportfolio.Adjustment"
10
+
11
+ def __init__(self, *args, **kwargs):
12
+ super().__init__(*args, **kwargs)
13
+ self.instrument_handler = InstrumentImportHandler(self.import_source)
14
+
15
+ def _deserialize(self, data):
16
+ data["date"] = datetime.strptime(data["date"], "%Y-%m-%d").date()
17
+ data["instrument"] = self.instrument_handler.process_object(
18
+ data["instrument"], only_security=True, read_only=True
19
+ )[0]
20
+ if data["factor"] is None:
21
+ raise DeserializationError("Can't process this data: no factor")
22
+
23
+ def _get_instance(self, data, history=None, **kwargs):
24
+ self.import_source.log += "\nGet Adjustment Instance."
25
+ self.import_source.log += f"\nParameter: Instrument={data['instrument']} Date={data['date']}"
26
+
27
+ if exact_adjustment := data["instrument"].pms_adjustments.filter(date=data["date"]).first():
28
+ return exact_adjustment
29
+ potential_adjustments = data["instrument"].pms_adjustments.filter(
30
+ date__gte=data["date"] - timedelta(days=7),
31
+ date__lte=data["date"] + timedelta(days=7),
32
+ factor=data["factor"],
33
+ )
34
+ if potential_adjustments.exists():
35
+ return potential_adjustments.first()
36
+
37
+ def _create_instance(self, data, **kwargs):
38
+ self.import_source.log += "\nCreate Adjustment datapoint."
39
+ return self.model.objects.create(**data, import_source=self.import_source)
@@ -0,0 +1,167 @@
1
+ from collections import defaultdict
2
+ from datetime import datetime
3
+ from decimal import Decimal
4
+ from itertools import chain
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from django.db import models
8
+ from wbcore.contrib.authentication.authentication import User
9
+ from wbcore.contrib.currency.import_export.handlers import CurrencyImportHandler
10
+ from wbcore.contrib.io.exceptions import DeserializationError
11
+ from wbcore.contrib.io.imports import ImportExportHandler
12
+ from wbcore.contrib.notifications.dispatch import send_notification
13
+ from wbfdm.import_export.handlers.instrument import InstrumentImportHandler
14
+ from wbfdm.import_export.handlers.instrument_price import InstrumentPriceImportHandler
15
+ from wbfdm.models.exchanges import Exchange
16
+ from wbportfolio.models.roles import PortfolioRole
17
+
18
+
19
+ class AssetPositionImportHandler(ImportExportHandler):
20
+ MODEL_APP_LABEL: str = "wbportfolio.AssetPosition"
21
+ PRICE_DATE_TIMEDELTA: int = 7
22
+ MAX_PRICE_DATE_TIMEDELTA: int = 360
23
+
24
+ def __init__(self, *args, **kwargs):
25
+ super().__init__(*args, **kwargs)
26
+ self.instrument_handler = InstrumentImportHandler(self.import_source)
27
+ self.instrument_price_handler = InstrumentPriceImportHandler(self.import_source)
28
+ self.currency_handler = CurrencyImportHandler(self.import_source)
29
+
30
+ def _deserialize(self, data: Dict[str, Any]):
31
+ from wbportfolio.models import Portfolio
32
+
33
+ portfolio_data = data.pop("portfolio", None)
34
+ underlying_instrument_data = data.pop("underlying_instrument", None)
35
+ if "currency" in data:
36
+ data["currency"] = self.currency_handler.process_object(data["currency"], read_only=True)[0]
37
+ data["date"] = datetime.strptime(data["date"], "%Y-%m-%d").date()
38
+ if data.get("asset_valuation_date", None):
39
+ data["asset_valuation_date"] = datetime.strptime(data["asset_valuation_date"], "%Y-%m-%d").date()
40
+ else:
41
+ data["asset_valuation_date"] = data["date"]
42
+
43
+ if exchange_data := data.pop("exchange", None):
44
+ sanitized_dict = {k: v for k, v in exchange_data.items() if v is not None}
45
+ if sanitized_dict:
46
+ data["exchange"] = Exchange.dict_to_model(sanitized_dict)
47
+
48
+ data["portfolio"] = Portfolio._get_or_create_portfolio(self.instrument_handler, portfolio_data)
49
+ data["underlying_instrument"] = self.instrument_handler.process_object(
50
+ underlying_instrument_data, only_security=False, read_only=True
51
+ )[0]
52
+
53
+ # number type deserialization and sanitization
54
+ # ensure the provided Decimal field are of type Decimal
55
+ decimal_fields = ["initial_currency_fx_rate", "initial_price", "initial_shares", "weighting"]
56
+ for field in decimal_fields:
57
+ if not (value := data.get(field, None)) is None:
58
+ data[field] = Decimal(value)
59
+
60
+ # Ensure that for shares and weighting, a None value default to 0
61
+ if "initial_shares" in data and data["initial_shares"] is None:
62
+ data["initial_shares"] = Decimal(0)
63
+ if "weighting" in data and data["weighting"] is None:
64
+ data["weighting"] = Decimal(0)
65
+
66
+ # if the initial price is not provided, we try to get it directly from the dataloader
67
+ if data.get("initial_price") is None:
68
+ try:
69
+ data["initial_price"] = data["underlying_instrument"].get_price(
70
+ data["date"], price_date_timedelta=self.PRICE_DATE_TIMEDELTA
71
+ )
72
+ except ValueError:
73
+ # If we cannot find a price with the default timedelta, we try with a bigger range (in case the instrument stopped trading for some time for instance)
74
+ try:
75
+ data["initial_price"] = data["underlying_instrument"].get_price(
76
+ data["date"], price_date_timedelta=self.MAX_PRICE_DATE_TIMEDELTA
77
+ )
78
+ except ValueError:
79
+ raise DeserializationError("Price not provided but can not be found automatically")
80
+
81
+ def _process_raw_data(self, data: Dict[str, Any]):
82
+ if prices := data.get("prices", None):
83
+ self.import_source.log += "Instrument Prices found: Importing"
84
+ self.instrument_price_handler.process({"data": prices})
85
+
86
+ def _get_instance(self, data: Dict[str, Any], history: Optional[models.QuerySet] = None, **kwargs) -> models.Model:
87
+ self.import_source.log += "\nTrying to get asset position instance"
88
+ position = data["portfolio"].assets.filter(
89
+ date=data["date"], underlying_instrument=data["underlying_instrument"]
90
+ )
91
+ if position.exists():
92
+ self.import_source.log += "\nAsset Position found."
93
+ if position.count() > 1:
94
+ position = position.filter(is_estimated=False)
95
+ if position.count() > 1:
96
+ raise ValueError(f'We should find only one Assetposition:{position.values_list("id", flat=True)}')
97
+ return position.first()
98
+ self.import_source.log += "\nAsset Position not found. A new one will be created"
99
+ return None
100
+
101
+ def _create_instance(self, data: Dict[str, Any], **kwargs) -> models.Model:
102
+ instance = self.model(
103
+ portfolio=data["portfolio"],
104
+ underlying_instrument=data["underlying_instrument"],
105
+ date=data["date"],
106
+ asset_valuation_date=data["asset_valuation_date"],
107
+ weighting=data.get("weighting", None),
108
+ initial_price=data["initial_price"],
109
+ initial_shares=data.get("initial_shares", None),
110
+ initial_currency_fx_rate=data.get("initial_currency_fx_rate", 1),
111
+ import_source=self.import_source,
112
+ currency=data.get("currency", None),
113
+ )
114
+
115
+ return self._save_object(instance, **kwargs)
116
+
117
+ def _save_object(self, _object, **kwargs):
118
+ _object.underlying_instrument_price = (
119
+ None # detech possibly already attached instrument price to retrigger the save mechanism
120
+ )
121
+ _object.save(create_underlying_instrument_price_if_missing=True)
122
+ return _object
123
+
124
+ def _post_processing_objects(
125
+ self,
126
+ created_objs: List[models.Model],
127
+ modified_objs: List[models.Model],
128
+ unmodified_objs: List[models.Model],
129
+ ):
130
+ from wbportfolio.models.portfolio import trigger_portfolio_change_as_task
131
+
132
+ portfolio_to_resynch = defaultdict(set)
133
+ imported_ids = defaultdict(set)
134
+ for obj in chain(created_objs, modified_objs, unmodified_objs):
135
+ portfolio_to_resynch[obj.portfolio].add(obj.date)
136
+ imported_ids[obj.portfolio].add(obj.id)
137
+
138
+ for portfolio, dates in portfolio_to_resynch.items():
139
+ # We remove leftovers positions from wrongly imported file
140
+ leftovers_positions = self.model.objects.filter(portfolio=portfolio, date__in=dates)
141
+ for obj_id in imported_ids[portfolio]:
142
+ leftovers_positions = leftovers_positions.exclude(id=obj_id)
143
+ for position in leftovers_positions:
144
+ position.delete()
145
+ for val_date in sorted(dates):
146
+ trigger_portfolio_change_as_task.delay(portfolio.id, val_date, recompute_weighting=True)
147
+
148
+ portfolios = set([(obj.portfolio, obj.date) for obj in chain(created_objs, modified_objs)])
149
+
150
+ for portfolio, val_date in portfolios:
151
+ custodian = portfolio.dependency_through.filter(type="CUSTODIAN")
152
+ if custodian.exists():
153
+ differences = portfolio.check_related_portfolio_at_date(
154
+ val_date, custodian.first().dependency_portfolio
155
+ )
156
+ if differences.exists():
157
+ for user in User.objects.filter(
158
+ profile_id__in=PortfolioRole.portfolio_managers().values_list("person", flat=True)
159
+ ):
160
+ send_notification(
161
+ code="wbportfolio.portfolio.check_custodian_portfolio",
162
+ title="There is a discrepency between two portfolios",
163
+ body=f"There has been a discrepency between two portfolios: {portfolio} and {custodian.first().dependency_portfolio}.",
164
+ user=user,
165
+ reverse_name="wbportfolio:portfolio-modelcompositionpandas-list",
166
+ reverse_args=[portfolio.id],
167
+ )