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,292 @@
1
+ import logging
2
+ from datetime import date, datetime
3
+ from decimal import Decimal
4
+ from typing import Any, Optional
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ from celery import shared_task
9
+ from celery.canvas import Signature
10
+ from django.db import models
11
+ from django.db.models import F
12
+ from django.utils import timezone
13
+ from wbfdm.enums import MarketData
14
+ from wbfdm.models.instruments import Instrument
15
+ from wbportfolio.models import Portfolio
16
+ from wbportfolio.models.transactions.trade_proposals import TradeProposal
17
+ from wbportfolio.pms.typing import Portfolio as PortfolioDTO
18
+ from wbportfolio.pms.typing import Position as PositionDTO
19
+
20
+ from .synchronization import SynchronizationTask
21
+
22
+
23
+ def convert_currency(x, val_date, other_currency):
24
+ instrument = Instrument.objects.get(id=x)
25
+ try:
26
+ return instrument.currency.convert(val_date, other_currency)
27
+ except Exception:
28
+ return np.nan
29
+
30
+
31
+ class PortfolioSynchronization(SynchronizationTask):
32
+ is_automatic_validation = models.BooleanField(
33
+ default=True,
34
+ verbose_name="Automatic validation",
35
+ help_text="Set to True if you want to automatically implement proposed positions",
36
+ )
37
+ propagate_history = models.BooleanField(
38
+ default=False,
39
+ verbose_name="Propagate History",
40
+ help_text="If true, when the depends on portfolio changes at a certain date, this method will trigger a synchronization for each date (at the scheduled frequency) from that date to the latest valid date",
41
+ )
42
+
43
+ def synchronize(
44
+ self,
45
+ portfolio: models.Model,
46
+ sync_date: date,
47
+ task_execution_datetime: Optional[datetime] = None,
48
+ override_execution_datetime_validity: Optional[bool] = False,
49
+ post_processing: bool = True,
50
+ **kwargs: Any,
51
+ ):
52
+ """
53
+ This function compute the new portfolio composition after synchronization (returns from `_import_method`) for a
54
+ given date and either update or create the portfolio or create a trade proposal given the new portfolio constituent.
55
+
56
+ :param portfolio: The portfolio to synchronize the positions from
57
+ :param sync_date: The date at which we need to synchronize the given portfolio
58
+ :param task_execution_datetime: An optional datetime specifying at which time this task was initially executed.
59
+ :param override_execution_datetime_validity: If true, we don't valide `task_execution_datetime`
60
+ :param kwargs: keyword arguments
61
+ """
62
+
63
+ initkwargs = {**kwargs, **self.cast_kwargs}
64
+ if not task_execution_datetime:
65
+ task_execution_datetime = timezone.now()
66
+ if portfolio.is_active_at_date(sync_date):
67
+ if self.is_valid_date(task_execution_datetime) or override_execution_datetime_validity:
68
+ if import_res := list(zip(*self._import_method(portfolio, sync_date, **initkwargs))):
69
+ effective_positions = list(filter(lambda x: x, import_res[0]))
70
+ target_positions = list(filter(lambda x: x, import_res[1]))
71
+ if len(target_positions) > 0:
72
+ target_portfolio = PortfolioDTO(target_positions)
73
+
74
+ effective_portfolio = (
75
+ PortfolioDTO(effective_positions) if len(effective_positions) > 0 else None
76
+ )
77
+ if self.is_automatic_validation:
78
+ # We process these positions automatically
79
+ portfolio.import_positions_at_date(
80
+ target_portfolio, sync_date, post_processing=post_processing
81
+ )
82
+ else:
83
+ trade_proposal, created = TradeProposal.objects.get_or_create(
84
+ trade_date=sync_date,
85
+ portfolio=portfolio,
86
+ defaults={"comment": "Automatic rebalancing"},
87
+ )
88
+ trade_proposal.create_or_update_trades(
89
+ target_portfolio=target_portfolio, effective_portfolio=effective_portfolio
90
+ )
91
+
92
+ portfolio.last_synchronization = timezone.now()
93
+ portfolio.save()
94
+
95
+ else:
96
+ logging.info(
97
+ f"Synchronization invalid: {portfolio.name} synchronization with {self.name} was triggered for {sync_date} but date not valid for crontab schedule {str(self.crontab)}"
98
+ )
99
+
100
+ def synchronize_as_task_si(self, portfolio: models.Model, sync_date: date, **kwargs: Any) -> Signature:
101
+ """
102
+ Utility function that returns the signature of the synchronize method
103
+ """
104
+ return synchronize_portfolio_as_task.si(self.id, portfolio.id, sync_date, **kwargs)
105
+
106
+ def _tasks_signature(self, sync_date: Optional[date] = None, **kwargs: Any) -> Signature:
107
+ """
108
+ Gather all tasks that needs to run under this synchronization job as a list of celery signatures.
109
+ This method is expected to be implemented at each inheriting class.
110
+ :param args: list
111
+ :param kwargs: dict
112
+ :return: list[signature]
113
+ """
114
+ for portfolio in self.portfolios.all():
115
+ portfolio_sync_dates = []
116
+ if sync_date:
117
+ portfolio_sync_dates = [sync_date]
118
+ elif not sync_date and portfolio.assets.exists() and (latest_asset := portfolio.assets.latest("date")):
119
+ portfolio_sync_dates = map(
120
+ lambda x: x.date(), pd.date_range(latest_asset.date, date.today(), freq="B", inclusive="left")
121
+ )
122
+ for portfolio_sync_date in portfolio_sync_dates:
123
+ if portfolio.is_active_at_date(portfolio_sync_date):
124
+ yield synchronize_portfolio_as_task.si(self.id, portfolio.id, portfolio_sync_date, **kwargs)
125
+
126
+ @classmethod
127
+ def _default_callback(
128
+ cls,
129
+ portfolio: Portfolio,
130
+ sync_date: date,
131
+ portfolio_created: Optional[Portfolio] = None,
132
+ adjusted_weighting: Optional[Decimal] = Decimal(1.0),
133
+ adjusted_currency_fx_rate: Optional[Decimal] = Decimal(1.0),
134
+ is_estimated: Optional[bool] = False,
135
+ portfolio_total_value: Optional[float] = None,
136
+ **kwargs: Any,
137
+ ):
138
+ """Recursively calculates the position for a portfolio
139
+
140
+ Arguments:
141
+ portfolio {portfolio.Portfolio} -- The Portfolio on which the assets will be computed
142
+ sync_date {datetime.date} -- The date on which the assets will be computed
143
+
144
+ Keyword Arguments:
145
+ portfolio {portfolio.Portfolio} -- The core portfolio from which the computed position are created (default: {None})
146
+ adjusted_weighting {int} -- the adjusted weight of the current level of index (default: {1})
147
+ adjusted_currency_fx_rate {int} -- the adjusted currency exchange rate on the current level of index (default: {1})
148
+
149
+ Yields:
150
+ tuple[dict, dict] -- Two dictionaries: One with filter parameters and one with default values
151
+ """
152
+ is_root_position_estimated = False
153
+ if not portfolio_created:
154
+ if portfolio_created := portfolio.primary_portfolio:
155
+ is_root_position_estimated = (
156
+ portfolio_created.assets.filter(date=sync_date).count() == 1
157
+ and portfolio_created.assets.filter(date=sync_date, is_estimated=True).count() == 1
158
+ )
159
+ if portfolio_created:
160
+ child_positions = portfolio_created.assets.filter(date=sync_date)
161
+ asset_positions = child_positions.all()
162
+ # Compute the total portfolio value based on the root position child (otherwise the value is passed as
163
+ # parameters in the recursion
164
+ if not portfolio_total_value:
165
+ portfolio_total_value = child_positions.aggregate(tv=models.Sum(F("total_value_fx_portfolio")))["tv"]
166
+ if not portfolio_total_value:
167
+ portfolio_total_value = portfolio_created.get_total_value(sync_date)
168
+ for position in child_positions:
169
+ if child_portfolio := position.underlying_instrument.portfolio:
170
+ if child_portfolio.assets.filter(date=sync_date).exists() and position.weighting is not None:
171
+ asset_positions = asset_positions.exclude(id=position.id)
172
+ yield from cls._default_callback(
173
+ portfolio,
174
+ sync_date,
175
+ portfolio_created=child_portfolio,
176
+ adjusted_weighting=position.weighting * adjusted_weighting,
177
+ portfolio_total_value=portfolio_total_value,
178
+ adjusted_currency_fx_rate=position.currency_fx_rate * adjusted_currency_fx_rate,
179
+ is_estimated=False
180
+ if is_root_position_estimated
181
+ else (is_estimated and position.is_estimated),
182
+ )
183
+ df = pd.DataFrame(
184
+ asset_positions.values_list(
185
+ "currency_fx_rate",
186
+ "price",
187
+ "weighting",
188
+ "shares",
189
+ "is_estimated",
190
+ "underlying_instrument",
191
+ "currency",
192
+ "exchange",
193
+ ),
194
+ columns=[
195
+ "currency_fx_rate",
196
+ "price",
197
+ "weighting",
198
+ "shares",
199
+ "is_estimated",
200
+ "underlying_instrument",
201
+ "currency",
202
+ "exchange",
203
+ ],
204
+ )
205
+ if not df.empty:
206
+ df.currency_fx_rate = df.currency_fx_rate * adjusted_currency_fx_rate
207
+ df.weighting = df.weighting * adjusted_weighting
208
+
209
+ df = (
210
+ df.groupby(["underlying_instrument", "currency", "exchange"], dropna=False)
211
+ .agg(
212
+ {
213
+ "currency_fx_rate": "first",
214
+ "price": "first",
215
+ "weighting": "sum",
216
+ "shares": "sum",
217
+ "is_estimated": "first",
218
+ }
219
+ )
220
+ .reset_index()
221
+ )
222
+ df[["underlying_instrument", "currency", "exchange"]] = df[
223
+ ["underlying_instrument", "currency", "exchange"]
224
+ ].astype("object")
225
+ df[["currency_fx_rate", "price", "weighting", "shares"]] = df[
226
+ ["currency_fx_rate", "price", "weighting", "shares"]
227
+ ].astype("float")
228
+
229
+ df["actual_currency_fx_rate"] = df.underlying_instrument.apply(
230
+ lambda x: convert_currency(x, sync_date, portfolio.currency)
231
+ ).astype("float")
232
+ df["actual_currency_fx_rate"] = df["actual_currency_fx_rate"].fillna(df["currency_fx_rate"])
233
+
234
+ df = df.where(pd.notnull(df), None).set_index("underlying_instrument")
235
+ missing_prices = df.loc[df["price"].isnull(), "price"]
236
+ if not missing_prices.empty:
237
+ prices_df = pd.DataFrame(
238
+ Instrument.objects.filter(id__in=missing_prices.index).dl.market_data(
239
+ values=[MarketData.CLOSE], exact_date=sync_date
240
+ )
241
+ )
242
+ if not prices_df.empty:
243
+ prices_df = prices_df[["close", "instrument_id"]].set_index("instrument_id").astype("float")
244
+ df.loc[prices_df.index, "price"] = prices_df
245
+
246
+ if portfolio_total_value is not None:
247
+ df["shares"] = (df["weighting"] * float(portfolio_total_value)) / (
248
+ df["price"] * df["actual_currency_fx_rate"]
249
+ )
250
+ if is_estimated:
251
+ df["is_estimated"] = True
252
+ for underlying_instrument, asset_position in df.to_dict("index").items():
253
+ if (
254
+ asset_position["weighting"] or asset_position["shares"]
255
+ ): # We don't yield empty position (pos with shares and weight equal to 0 or None)
256
+ # We return the position as a serialized dictionary
257
+ yield None, PositionDTO(
258
+ date=sync_date,
259
+ asset_valuation_date=sync_date,
260
+ portfolio_created=portfolio_created.id,
261
+ underlying_instrument=underlying_instrument,
262
+ instrument_type=Instrument.objects.get(id=underlying_instrument).security_instrument_type,
263
+ currency=asset_position["currency"],
264
+ exchange=asset_position["exchange"],
265
+ shares=asset_position["shares"],
266
+ price=asset_position["price"],
267
+ currency_fx_rate=asset_position["actual_currency_fx_rate"],
268
+ weighting=asset_position["weighting"],
269
+ is_estimated=asset_position["is_estimated"],
270
+ )
271
+
272
+ def __str__(self) -> str:
273
+ return self.name
274
+
275
+ @classmethod
276
+ def get_representation_endpoint(cls) -> str:
277
+ return "wbportfolio:portfoliosynchronizationrepresentation-list"
278
+
279
+ @classmethod
280
+ def get_representation_value_key(cls) -> str:
281
+ return "id"
282
+
283
+ @classmethod
284
+ def get_representation_label_key(cls) -> str:
285
+ return "{{name}}"
286
+
287
+
288
+ @shared_task(queue="portfolio")
289
+ def synchronize_portfolio_as_task(synchronization_method_id: int, portfolio_id: int, sync_date: date, **kwargs: Any):
290
+ portfolio = Portfolio.objects.get(id=portfolio_id)
291
+ synchronization_method = PortfolioSynchronization.objects.get(id=synchronization_method_id)
292
+ synchronization_method.synchronize(portfolio, sync_date, **kwargs)
@@ -0,0 +1,200 @@
1
+ import logging
2
+ import math
3
+ from datetime import date, datetime, timedelta
4
+ from decimal import Decimal
5
+ from typing import Any, Optional
6
+
7
+ import pandas as pd
8
+ from celery import shared_task
9
+ from celery.canvas import Signature
10
+ from django.db import models
11
+ from django.utils import timezone
12
+ from pandas.tseries.offsets import BDay
13
+ from wbfdm.models import Instrument
14
+ from wbfdm.models.instruments.instrument_prices import InstrumentPrice
15
+
16
+ from .synchronization import SynchronizationTask
17
+
18
+
19
+ class PriceComputation(SynchronizationTask):
20
+ @property
21
+ def instruments(self):
22
+ for instrument in Instrument.active_objects.filter(
23
+ models.Q(id__in=self.products.values("id")) | models.Q(id__in=self.indexes.values("id"))
24
+ ):
25
+ yield instrument
26
+
27
+ def compute(
28
+ self,
29
+ instrument: models.Model,
30
+ sync_date: date,
31
+ task_execution_datetime: Optional[datetime] = None,
32
+ override_execution_datetime_validity: Optional[bool] = False,
33
+ **kwargs: Any,
34
+ ):
35
+ """
36
+ Given positions at t and t-1, we compute the performance and estimate the instrument price at t
37
+ If a price already exists at that date and is estimated already, we update it. If no price exists, we create it as estimated.
38
+ Otherwise, we don't do anything to protect imported real prices.
39
+ :param instrument: The instrument to compute the new price from
40
+ :param sync_date: The date at which we need to compute the new price
41
+ :param task_execution_datetime: An optional datetime specifying at which time this task was initially executed.
42
+ :param override_execution_datetime_validity: If true, we don't valide `task_execution_datetime`
43
+ :param kwargs: keyword arguments
44
+ """
45
+ if not task_execution_datetime:
46
+ task_execution_datetime = timezone.now()
47
+
48
+ if (
49
+ (self.is_valid_date(task_execution_datetime) or override_execution_datetime_validity)
50
+ and instrument.is_active_at_date(sync_date)
51
+ and not sync_date.weekday() in [5, 6]
52
+ ):
53
+ price_data = self._import_method(instrument, sync_date, **kwargs)
54
+ if (
55
+ price_data
56
+ and (_instrument := price_data.pop("instrument", None))
57
+ and (_date := price_data.pop("date", None))
58
+ ):
59
+ calculated = price_data.pop("calculated", True)
60
+ try:
61
+ price = InstrumentPrice.objects.get(instrument=_instrument, date=_date, calculated=calculated)
62
+ for k, v in price_data.items():
63
+ setattr(price, k, v)
64
+ price.save()
65
+ except InstrumentPrice.DoesNotExist:
66
+ price = InstrumentPrice.objects.create(
67
+ instrument=_instrument, date=_date, calculated=calculated, **price_data
68
+ )
69
+
70
+ price.save() # trigger explicitly save logic as update_or_create doesn't
71
+ if (
72
+ _date == _instrument.prices.latest("date").date
73
+ ): # if price date is the latest instrument price date, we recomputet the last valuation data
74
+ _instrument.update_last_valuation_date()
75
+ else:
76
+ logging.info(
77
+ f"Price Computation invalid: {str(instrument)} price computation with {self.name} was triggered for {sync_date} but date not valid for crontab schedule {str(self.crontab)}"
78
+ )
79
+
80
+ def compute_price_as_task_si(self, instrument: models.Model, sync_date: date, **kwargs: Any) -> Signature:
81
+ """
82
+ Utility function that returns the signature of the compute method
83
+ """
84
+ return compute_price_as_task.si(self.id, instrument.id, sync_date, **kwargs)
85
+
86
+ def _tasks_signature(
87
+ self, sync_date: Optional[date] = None, to_date: Optional[date] = None, **kwargs: Any
88
+ ) -> Signature:
89
+ """
90
+ Gather all tasks that needs to run under this synchronization job as a list of celery signatures.
91
+ This method is expected to be implemented at each inheriting class.
92
+ :param args: list
93
+ :param kwargs: dict
94
+ :return: list[signature]
95
+ """
96
+ if not to_date:
97
+ to_date = date.today()
98
+ for instrument in self.instruments:
99
+ # Get latest valuation date + 1 Bday if not given by the key word arguments
100
+ instrument_sync_dates = []
101
+ if sync_date:
102
+ instrument_sync_dates = [sync_date]
103
+ elif not sync_date and instrument.prices.exists() and (last_price := instrument.prices.latest("date")):
104
+ instrument_sync_dates = map(
105
+ lambda x: x.date(),
106
+ pd.date_range(
107
+ max(last_price.date, to_date - timedelta(days=7)),
108
+ to_date,
109
+ freq="B",
110
+ inclusive="left",
111
+ ),
112
+ )
113
+ for instrument_sync_date in instrument_sync_dates:
114
+ if instrument.is_active_at_date(instrument_sync_date):
115
+ yield compute_price_as_task.si(self.id, instrument.id, instrument_sync_date, **kwargs)
116
+
117
+ @classmethod
118
+ def _default_callback(cls, instrument: Instrument, val_date: date, **kwargs: Any):
119
+ """
120
+ Default NAV computation function. We simply compute the performance given two positions for two dates and estimate the new price
121
+ based on the overall performance.
122
+ TODO: If exit/buy of positions, is this function still correct?
123
+ :param instrument: The instrument to compute the new price from
124
+ :param val_date: The date at which we need to compute the new price
125
+ :param kwargs: keyword arguments
126
+ """
127
+ if portfolio := instrument.portfolio:
128
+ # check if the asset portfolio of this instruments exists and has positions at the synchronization date
129
+ if previous_date := portfolio.get_latest_asset_position_date(val_date - BDay(1), with_estimated=True):
130
+ # Get the previous valid price date before sync_date, checks if it exists and if positions are available
131
+ # at that date.
132
+ # If asset position exists on the previous day but not at the sync date, maybe propagation were not done, and we try it
133
+ if portfolio.assets.filter(date=previous_date).exists():
134
+ portfolio.propagate_or_update_assets(previous_date, val_date)
135
+ if portfolio.assets.filter(date=val_date).exists():
136
+ last_price = None
137
+ if (
138
+ last_valuation := instrument.prices.filter(date=previous_date)
139
+ .order_by("calculated")
140
+ .first()
141
+ ):
142
+ last_price = last_valuation.net_value
143
+ elif not instrument.valuations.filter(date__lt=previous_date).exists():
144
+ last_price = instrument.issue_price
145
+ if last_price:
146
+ weights = pd.DataFrame(
147
+ portfolio.assets.filter(date=previous_date).values(
148
+ "weighting", "date", "underlying_instrument"
149
+ )
150
+ )
151
+ weights = weights.pivot_table(
152
+ index="date", columns=["underlying_instrument"], values="weighting", aggfunc="sum"
153
+ ).astype("float")
154
+ weights = weights.iloc[-1, :]
155
+ perfs = pd.DataFrame(
156
+ portfolio.assets.filter(date__in=[previous_date, val_date]).values(
157
+ "date", "price_fx_portfolio", "underlying_instrument"
158
+ )
159
+ )
160
+ perfs = perfs.pivot_table(
161
+ index="date",
162
+ columns=["underlying_instrument"],
163
+ values="price_fx_portfolio",
164
+ aggfunc="mean",
165
+ ).astype("float")
166
+ perfs = perfs / perfs.shift(1, axis=0) - 1.0
167
+ perfs = perfs.fillna(0).iloc[-1, :]
168
+ total_perfs = float((perfs * weights).sum())
169
+ new_gross_valuation = float(last_price) * (1.0 + total_perfs)
170
+ if new_gross_valuation and not math.isnan(new_gross_valuation):
171
+ return {
172
+ "instrument": instrument,
173
+ "date": val_date,
174
+ "gross_value": Decimal(new_gross_valuation),
175
+ "net_value": Decimal(new_gross_valuation),
176
+ }
177
+
178
+ return None
179
+
180
+ def __str__(self) -> str:
181
+ return self.name
182
+
183
+ @classmethod
184
+ def get_representation_endpoint(cls) -> str:
185
+ return "wbportfolio:pricecomputationrepresentation-list"
186
+
187
+ @classmethod
188
+ def get_representation_value_key(cls) -> str:
189
+ return "id"
190
+
191
+ @classmethod
192
+ def get_representation_label_key(cls) -> str:
193
+ return "{{name}}"
194
+
195
+
196
+ @shared_task(queue="portfolio")
197
+ def compute_price_as_task(price_computation_method_id: int, instrument_id: int, sync_date: date, **kwargs: Any):
198
+ instrument = Instrument.objects.get(id=instrument_id)
199
+ price_computation = PriceComputation.objects.get(id=price_computation_method_id)
200
+ price_computation.compute(instrument, sync_date, **kwargs)
@@ -0,0 +1,188 @@
1
+ import importlib
2
+ from collections.abc import Callable
3
+ from datetime import date, datetime, time
4
+ from json import loads
5
+ from typing import Any, Iterator, Optional
6
+
7
+ from celery import chord, group, shared_task
8
+ from celery.canvas import Signature, signature
9
+ from croniter import croniter, croniter_range
10
+ from django.contrib.contenttypes.models import ContentType
11
+ from django.db import models
12
+ from django.db.models.signals import post_save
13
+ from django.dispatch import receiver
14
+ from django.utils import timezone
15
+ from django_celery_beat.models import CrontabSchedule, PeriodicTask, cronexp
16
+
17
+
18
+ class SynchronizationTask(PeriodicTask):
19
+ RELATIVE_TASK_MODULE_PATH = "wbportfolio.models.synchronization.synchronization.task"
20
+
21
+ dependent_task = models.ForeignKey(
22
+ "self", related_name="dependency_tasks", null=True, blank=True, on_delete=models.SET_NULL
23
+ )
24
+ import_path = models.CharField(max_length=512, verbose_name="Import Path", default="", blank=True)
25
+
26
+ @property
27
+ def cast_args(self) -> list[Any]:
28
+ """
29
+ transform the string args representation into list. We except this to become unnecessary when django beat move to jsonfield
30
+ :return: list
31
+ """
32
+ return loads(self.args or "[]")
33
+
34
+ @property
35
+ def cast_kwargs(self) -> dict[Any, Any]:
36
+ """
37
+ transform the string kwargs representation into dictionary. We except this to become unnecessary when django beat move to jsonfield
38
+ :return: list
39
+ """
40
+ return loads(self.kwargs or "{}")
41
+
42
+ @property
43
+ def _import_method(self) -> Callable[[Any], Any]:
44
+ """
45
+ If a custom task is specified, return the loaded module as callabck. Otherwise, returns the default synchronization
46
+ function defined in `_default_callback
47
+ :return: Callable
48
+ """
49
+ if import_path := self.import_path:
50
+ synchronize_module = importlib.import_module(import_path)
51
+ return synchronize_module.callback
52
+ else:
53
+ return self._default_callback
54
+
55
+ def schedule_str(self, filter_daily: Optional[bool] = False) -> str:
56
+ """
57
+ Returns the crontab string representation. If fitler_daily is true, we cast the crontab so that the lowest frequency
58
+ becomes daily.
59
+ :param filter_daily: bool (optional)
60
+ :return: crontab string representation
61
+ """
62
+ crontab_minute = cronexp(self.crontab.minute)
63
+ crontab_hour = cronexp(self.crontab.hour)
64
+ if filter_daily:
65
+ if crontab_minute == "*":
66
+ crontab_minute = "0"
67
+ if crontab_hour == "*":
68
+ crontab_hour = "0"
69
+ return "{0} {1} {2} {3} {4}".format(
70
+ crontab_minute,
71
+ crontab_hour,
72
+ cronexp(self.crontab.day_of_month),
73
+ cronexp(self.crontab.month_of_year),
74
+ cronexp(self.crontab.day_of_week),
75
+ )
76
+
77
+ def dates_range(self, from_date: date, to_date: date, filter_daily: Optional[bool] = False) -> Iterator[datetime]:
78
+ """
79
+ returns a list of valid dates given an interval and a specific crontab schedule.
80
+ :param from_date: date
81
+ :param to_date: date
82
+ :param filter_daily: bool (optional)
83
+ :return: list[date]
84
+ """
85
+ min_datetime = datetime.combine(from_date, time.min)
86
+ max_datetime = datetime.combine(to_date, time.max)
87
+ return croniter_range(min_datetime, max_datetime, self.schedule_str(filter_daily=filter_daily))
88
+
89
+ def is_valid_date(self, sync_datetime: datetime) -> bool:
90
+ """
91
+ check wether a date is valid given the stored crontab schedule
92
+ :param sync_datetime: datetime
93
+ :return: bool
94
+ """
95
+ return croniter.match(self.schedule_str(), sync_datetime)
96
+
97
+ def save(self, *args: Any, **kwargs: Any):
98
+ self.task = self.RELATIVE_TASK_MODULE_PATH
99
+ if not self.schedule:
100
+ self.crontab, _ = CrontabSchedule.objects.get_or_create(
101
+ minute="0",
102
+ hour="*",
103
+ day_of_week="*",
104
+ day_of_month="*",
105
+ month_of_year="*",
106
+ )
107
+ if not self.crontab:
108
+ raise ValueError("Synchronization task supports only Crontab Schedule")
109
+ if self.crontab.minute == "*":
110
+ raise ValueError("The minimum crontab frequency supported is hourly.")
111
+ super().save(*args, **kwargs)
112
+
113
+ def _tasks_signature(self, *args, **kwargs: Any) -> Signature:
114
+ """
115
+ Gather all tasks that needs to run under this synchronization job as a list of celery signatures.
116
+ This method is expected to be implemented at each inheriting class.
117
+ :param args: list
118
+ :param kwargs: dict
119
+ :return: list[signature]
120
+ """
121
+ raise NotImplementedError()
122
+
123
+ def _default_callback(self, *args: Any, **kwargs: Any) -> Any:
124
+ """
125
+ The default synchronization function executed if no custom task is provided for this synchronization object.
126
+ This method is expected to be implemented at each inheriting class.
127
+ :param args: list
128
+ :param kwargs: dict
129
+ :return: callable
130
+ """
131
+ raise NotImplementedError()
132
+
133
+ def _get_kwargs(self) -> Any:
134
+ """
135
+ return the base keyword argument to be injected into the `_tasks_signature` method. Define as a standalone function
136
+ in order to allow subclass definition.
137
+ :return: dict
138
+ """
139
+ return {"task_execution_datetime": timezone.now()}
140
+
141
+ def _end_task_signature(self, **kwargs: Any) -> Signature:
142
+ """
143
+ A synchronization object can defined a dependant task that will be executed at this end of all returned task from
144
+ _tasks_signatures.
145
+ This function returns the signature of this chained task.
146
+ :param kwargs:
147
+ :return: signature
148
+ """
149
+ if self.dependent_task:
150
+ kwargs = {"override_execution_datetime_validity": True, **self.dependent_task.cast_kwargs, **kwargs}
151
+ return signature(
152
+ self.dependent_task.task, args=self.dependent_task.cast_args, kwargs=kwargs, immutable=True
153
+ )
154
+ return None
155
+
156
+ def chord(self, **kwargs: Any) -> chord:
157
+ """
158
+ This function is the main entry point of the synchronization worklow. It is called from within the shared_task `task`
159
+ and create the celery chord containing the list of tasks chained to the end task (if any)
160
+ :param kwargs:
161
+ :return: chord
162
+ """
163
+ kwargs = {**kwargs, **self._get_kwargs()}
164
+ tasks = list(self._tasks_signature(**kwargs))
165
+ if end_task := self._end_task_signature(**kwargs):
166
+ return chord(tasks, end_task)
167
+ return group(tasks)
168
+
169
+
170
+ @receiver(post_save, sender="wbportfolio.SynchronizationTask")
171
+ @receiver(post_save, sender="wbportfolio.PortfolioSynchronization")
172
+ @receiver(post_save, sender="wbportfolio.PriceComputation")
173
+ def post_save_synchronization_task(sender, instance: models.Model, created: bool, raw: bool, **kwargs: Any):
174
+ """
175
+ Ensure args attribute contains the necessary arguments to retrieve the calling job from within asynchronous task
176
+ """
177
+ if (created and not raw) or not instance.args:
178
+ content_type = ContentType.objects.get_for_model(instance)
179
+ instance.args = f'["{instance.id}", "{content_type.app_label}", "{content_type.model}"]'
180
+ instance.save()
181
+
182
+
183
+ @shared_task
184
+ def task(synchronization_object_id: int, app_label: str, model: str, **kwargs: Any):
185
+ synchronization_object = ContentType.objects.get(app_label=app_label, model=model).get_object_for_this_type(
186
+ id=synchronization_object_id
187
+ )
188
+ synchronization_object.chord(**kwargs).apply_async()