wbportfolio 1.44.5__py2.py3-none-any.whl → 1.45.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 (317) hide show
  1. wbportfolio/admin/__init__.py +1 -1
  2. wbportfolio/admin/asset.py +2 -1
  3. wbportfolio/admin/custodians.py +1 -0
  4. wbportfolio/admin/indexes.py +15 -0
  5. wbportfolio/admin/portfolio.py +12 -7
  6. wbportfolio/admin/portfolio_relationships.py +1 -0
  7. wbportfolio/admin/product_groups.py +2 -0
  8. wbportfolio/admin/products.py +2 -1
  9. wbportfolio/admin/reconciliations.py +1 -0
  10. wbportfolio/admin/registers.py +1 -0
  11. wbportfolio/admin/roles.py +1 -0
  12. wbportfolio/admin/transactions/__init__.py +1 -0
  13. wbportfolio/admin/transactions/claim.py +1 -0
  14. wbportfolio/admin/transactions/dividends.py +1 -0
  15. wbportfolio/admin/transactions/fees.py +1 -0
  16. wbportfolio/admin/transactions/rebalancing.py +26 -0
  17. wbportfolio/admin/transactions/trades.py +4 -3
  18. wbportfolio/admin/transactions/transactions.py +1 -0
  19. wbportfolio/analysis/claims.py +2 -1
  20. wbportfolio/contrib/company_portfolio/models.py +3 -6
  21. wbportfolio/contrib/company_portfolio/tests/conftest.py +0 -12
  22. wbportfolio/contrib/company_portfolio/tests/test_models.py +1 -0
  23. wbportfolio/defaults/fees/default.py +1 -0
  24. wbportfolio/factories/__init__.py +1 -7
  25. wbportfolio/factories/adjustments.py +1 -0
  26. wbportfolio/factories/assets.py +13 -7
  27. wbportfolio/factories/claim.py +1 -0
  28. wbportfolio/factories/custodians.py +1 -0
  29. wbportfolio/factories/dividends.py +1 -0
  30. wbportfolio/factories/fees.py +1 -0
  31. wbportfolio/factories/indexes.py +1 -0
  32. wbportfolio/factories/portfolio_cash_flow.py +1 -0
  33. wbportfolio/factories/portfolio_cash_targets.py +1 -0
  34. wbportfolio/factories/portfolio_swing_pricings.py +1 -0
  35. wbportfolio/factories/portfolios.py +3 -0
  36. wbportfolio/factories/product_groups.py +1 -0
  37. wbportfolio/factories/products.py +1 -0
  38. wbportfolio/factories/rebalancing.py +23 -0
  39. wbportfolio/factories/reconciliations.py +1 -0
  40. wbportfolio/factories/roles.py +1 -0
  41. wbportfolio/factories/trades.py +1 -0
  42. wbportfolio/factories/transactions.py +1 -0
  43. wbportfolio/fdm/tasks.py +1 -0
  44. wbportfolio/filters/__init__.py +1 -1
  45. wbportfolio/filters/assets.py +8 -9
  46. wbportfolio/filters/assets_and_net_new_money_progression.py +1 -0
  47. wbportfolio/filters/custodians.py +1 -0
  48. wbportfolio/filters/esg.py +1 -0
  49. wbportfolio/filters/performances.py +7 -6
  50. wbportfolio/filters/portfolios.py +21 -1
  51. wbportfolio/filters/positions.py +1 -0
  52. wbportfolio/filters/products.py +1 -0
  53. wbportfolio/filters/roles.py +1 -0
  54. wbportfolio/filters/signals.py +1 -0
  55. wbportfolio/filters/transactions/claim.py +1 -0
  56. wbportfolio/filters/transactions/fees.py +1 -0
  57. wbportfolio/filters/transactions/trades.py +2 -1
  58. wbportfolio/filters/transactions/transactions.py +1 -0
  59. wbportfolio/import_export/backends/ubs/mixin.py +1 -0
  60. wbportfolio/import_export/backends/wbfdm/adjustment.py +1 -0
  61. wbportfolio/import_export/handlers/asset_position.py +11 -13
  62. wbportfolio/import_export/handlers/fees.py +1 -0
  63. wbportfolio/import_export/handlers/portfolio_cash_flow.py +1 -0
  64. wbportfolio/import_export/handlers/trade.py +1 -0
  65. wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +1 -0
  66. wbportfolio/import_export/parsers/jpmorgan/fees.py +1 -0
  67. wbportfolio/import_export/parsers/jpmorgan/strategy.py +5 -4
  68. wbportfolio/import_export/parsers/jpmorgan/valuation.py +1 -0
  69. wbportfolio/import_export/parsers/leonteq/customer_trade.py +1 -0
  70. wbportfolio/import_export/parsers/leonteq/equity.py +13 -12
  71. wbportfolio/import_export/parsers/leonteq/fees.py +1 -0
  72. wbportfolio/import_export/parsers/leonteq/trade.py +1 -0
  73. wbportfolio/import_export/parsers/leonteq/valuation.py +1 -0
  74. wbportfolio/import_export/parsers/natixis/customer_trade.py +1 -0
  75. wbportfolio/import_export/parsers/natixis/d1_customer_trade.py +1 -0
  76. wbportfolio/import_export/parsers/natixis/d1_equity.py +3 -2
  77. wbportfolio/import_export/parsers/natixis/d1_fees.py +1 -0
  78. wbportfolio/import_export/parsers/natixis/d1_trade.py +1 -0
  79. wbportfolio/import_export/parsers/natixis/d1_valuation.py +1 -0
  80. wbportfolio/import_export/parsers/natixis/equity.py +5 -5
  81. wbportfolio/import_export/parsers/natixis/trade.py +1 -0
  82. wbportfolio/import_export/parsers/natixis/utils.py +8 -7
  83. wbportfolio/import_export/parsers/sg_lux/custodian_positions.py +1 -0
  84. wbportfolio/import_export/parsers/sg_lux/customer_trade.py +1 -0
  85. wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +2 -1
  86. wbportfolio/import_export/parsers/sg_lux/customer_trade_slk.py +2 -1
  87. wbportfolio/import_export/parsers/sg_lux/customer_trade_without_pw.py +1 -0
  88. wbportfolio/import_export/parsers/sg_lux/equity.py +7 -8
  89. wbportfolio/import_export/parsers/sg_lux/portfolio_cash_flow.py +1 -0
  90. wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py +1 -0
  91. wbportfolio/import_export/parsers/sg_lux/registers.py +2 -1
  92. wbportfolio/import_export/parsers/societe_generale/customer_trade.py +1 -0
  93. wbportfolio/import_export/parsers/societe_generale/strategy.py +8 -9
  94. wbportfolio/import_export/parsers/societe_generale/valuation.py +1 -0
  95. wbportfolio/import_export/parsers/tellco/equity.py +5 -4
  96. wbportfolio/import_export/parsers/ubs/api/asset_position.py +15 -14
  97. wbportfolio/import_export/parsers/ubs/api/fees.py +1 -0
  98. wbportfolio/import_export/parsers/ubs/customer_trade.py +1 -0
  99. wbportfolio/import_export/parsers/ubs/equity.py +3 -2
  100. wbportfolio/import_export/parsers/ubs/historical_customer_trade.py +1 -0
  101. wbportfolio/import_export/parsers/ubs/valuation.py +1 -0
  102. wbportfolio/import_export/parsers/vontobel/asset_position.py +19 -19
  103. wbportfolio/import_export/parsers/vontobel/customer_trade.py +1 -0
  104. wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +1 -0
  105. wbportfolio/import_export/parsers/vontobel/management_fees.py +1 -0
  106. wbportfolio/import_export/parsers/vontobel/performance_fees.py +1 -0
  107. wbportfolio/import_export/parsers/vontobel/trade.py +1 -0
  108. wbportfolio/import_export/parsers/vontobel/valuation_api.py +23 -0
  109. wbportfolio/import_export/resources/assets.py +4 -3
  110. wbportfolio/import_export/resources/trades.py +1 -0
  111. wbportfolio/metric/backends/base.py +1 -0
  112. wbportfolio/metric/backends/portfolio_base.py +1 -0
  113. wbportfolio/metric/backends/portfolio_esg.py +1 -0
  114. wbportfolio/metric/tests/test_portfolio_base.py +1 -0
  115. wbportfolio/migrations/0052_remove_cash_instrument_ptr_and_more.py +1 -131
  116. wbportfolio/migrations/0067_assetposition_unique_asset_position.py +1 -1
  117. wbportfolio/migrations/0070_remove_assetposition_unique_asset_position_and_more.py +1 -1
  118. wbportfolio/migrations/0073_remove_product_price_computation_and_more.py +407 -0
  119. wbportfolio/models/__init__.py +0 -5
  120. wbportfolio/models/adjustments.py +8 -2
  121. wbportfolio/models/asset.py +117 -98
  122. wbportfolio/models/graphs/portfolio.py +161 -0
  123. wbportfolio/models/graphs/utils.py +83 -0
  124. wbportfolio/models/indexes.py +2 -13
  125. wbportfolio/models/mixins/instruments.py +28 -8
  126. wbportfolio/models/portfolio.py +538 -332
  127. wbportfolio/models/portfolio_cash_flow.py +1 -0
  128. wbportfolio/models/portfolio_relationship.py +6 -2
  129. wbportfolio/models/product_groups.py +3 -2
  130. wbportfolio/models/products.py +3 -17
  131. wbportfolio/models/reconciliations/account_reconciliation_lines.py +1 -0
  132. wbportfolio/models/reconciliations/account_reconciliations.py +1 -0
  133. wbportfolio/models/registers.py +1 -0
  134. wbportfolio/models/transactions/__init__.py +1 -0
  135. wbportfolio/models/transactions/claim.py +8 -8
  136. wbportfolio/models/transactions/dividends.py +1 -0
  137. wbportfolio/models/transactions/fees.py +1 -0
  138. wbportfolio/models/transactions/rebalancing.py +153 -0
  139. wbportfolio/models/transactions/trade_proposals.py +153 -155
  140. wbportfolio/models/transactions/trades.py +48 -40
  141. wbportfolio/models/transactions/transactions.py +6 -12
  142. wbportfolio/models/utils.py +1 -0
  143. wbportfolio/pms/analytics/__init__.py +0 -0
  144. wbportfolio/pms/analytics/portfolio.py +28 -0
  145. wbportfolio/pms/trading/handler.py +13 -16
  146. wbportfolio/pms/typing.py +13 -29
  147. wbportfolio/rebalancing/__init__.py +0 -0
  148. wbportfolio/rebalancing/base.py +16 -0
  149. wbportfolio/rebalancing/decorators.py +17 -0
  150. wbportfolio/rebalancing/models/__init__.py +3 -0
  151. wbportfolio/rebalancing/models/composite.py +31 -0
  152. wbportfolio/rebalancing/models/equally_weighted.py +21 -0
  153. wbportfolio/rebalancing/models/model_portfolio.py +35 -0
  154. wbportfolio/reports/monthly_position_report.py +1 -1
  155. wbportfolio/risk_management/backends/accounts.py +7 -6
  156. wbportfolio/risk_management/backends/controversy_portfolio.py +1 -0
  157. wbportfolio/risk_management/backends/exposure_portfolio.py +1 -0
  158. wbportfolio/risk_management/backends/instrument_list_portfolio.py +1 -0
  159. wbportfolio/risk_management/backends/liquidity_risk.py +1 -0
  160. wbportfolio/risk_management/backends/liquidity_stress_instrument.py +1 -0
  161. wbportfolio/risk_management/backends/mixins.py +1 -0
  162. wbportfolio/risk_management/backends/product_integrity.py +6 -1
  163. wbportfolio/risk_management/backends/stop_loss_instrument.py +1 -0
  164. wbportfolio/risk_management/backends/stop_loss_portfolio.py +1 -0
  165. wbportfolio/risk_management/backends/ucits_portfolio.py +12 -5
  166. wbportfolio/risk_management/tests/test_accounts.py +1 -0
  167. wbportfolio/risk_management/tests/test_controversy_portfolio.py +1 -0
  168. wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -0
  169. wbportfolio/risk_management/tests/test_instrument_list_portfolio.py +1 -0
  170. wbportfolio/risk_management/tests/test_liquidity_risk.py +1 -0
  171. wbportfolio/risk_management/tests/test_product_integrity.py +1 -0
  172. wbportfolio/risk_management/tests/test_stop_loss_instrument.py +1 -0
  173. wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -0
  174. wbportfolio/risk_management/tests/test_ucits_portfolio.py +2 -1
  175. wbportfolio/serializers/__init__.py +5 -5
  176. wbportfolio/serializers/adjustments.py +1 -0
  177. wbportfolio/serializers/assets.py +18 -19
  178. wbportfolio/serializers/custodians.py +1 -0
  179. wbportfolio/serializers/portfolio_cash_flow.py +1 -0
  180. wbportfolio/serializers/portfolio_cash_targets.py +1 -0
  181. wbportfolio/serializers/portfolio_relationship.py +1 -0
  182. wbportfolio/serializers/portfolio_swing_pricing.py +1 -0
  183. wbportfolio/serializers/portfolios.py +61 -40
  184. wbportfolio/serializers/positions.py +1 -0
  185. wbportfolio/serializers/product_group.py +1 -0
  186. wbportfolio/serializers/products.py +4 -7
  187. wbportfolio/serializers/rebalancing.py +57 -0
  188. wbportfolio/serializers/reconciliations.py +2 -1
  189. wbportfolio/serializers/registers.py +1 -0
  190. wbportfolio/serializers/roles.py +1 -0
  191. wbportfolio/serializers/signals.py +10 -15
  192. wbportfolio/serializers/transactions/__init__.py +1 -1
  193. wbportfolio/serializers/transactions/claim.py +1 -0
  194. wbportfolio/serializers/transactions/fees.py +1 -0
  195. wbportfolio/serializers/transactions/trade_proposals.py +85 -0
  196. wbportfolio/serializers/transactions/trades.py +9 -51
  197. wbportfolio/serializers/transactions/transactions.py +4 -3
  198. wbportfolio/tasks.py +1 -78
  199. wbportfolio/tests/conftest.py +6 -13
  200. wbportfolio/tests/models/test_account_reconciliation.py +2 -0
  201. wbportfolio/tests/models/test_assets.py +27 -19
  202. wbportfolio/tests/models/test_customer_trades.py +1 -0
  203. wbportfolio/tests/models/test_imports.py +5 -1
  204. wbportfolio/tests/models/test_merge.py +5 -4
  205. wbportfolio/tests/models/test_portfolio_cash_flow.py +8 -6
  206. wbportfolio/tests/models/test_portfolios.py +619 -154
  207. wbportfolio/tests/models/test_product_groups.py +1 -0
  208. wbportfolio/tests/models/test_products.py +6 -3
  209. wbportfolio/tests/models/test_roles.py +1 -0
  210. wbportfolio/tests/models/test_splits.py +1 -0
  211. wbportfolio/tests/models/transactions/test_claim.py +1 -0
  212. wbportfolio/tests/models/transactions/test_fees.py +1 -0
  213. wbportfolio/tests/models/transactions/test_rebalancing.py +81 -0
  214. wbportfolio/tests/models/transactions/test_trades.py +1 -0
  215. wbportfolio/tests/models/utils.py +1 -0
  216. wbportfolio/tests/pms/__init__.py +0 -0
  217. wbportfolio/tests/pms/test_analytics.py +35 -0
  218. wbportfolio/tests/rebalancing/__init__.py +0 -0
  219. wbportfolio/tests/rebalancing/test_models.py +127 -0
  220. wbportfolio/tests/serializers/test_claims.py +1 -0
  221. wbportfolio/tests/signals.py +1 -7
  222. wbportfolio/tests/tests.py +2 -0
  223. wbportfolio/tests/viewsets/test_assets.py +1 -0
  224. wbportfolio/tests/viewsets/test_performances.py +1 -0
  225. wbportfolio/tests/viewsets/test_products.py +1 -0
  226. wbportfolio/tests/viewsets/transactions/test_claims.py +1 -0
  227. wbportfolio/urls.py +26 -12
  228. wbportfolio/viewsets/__init__.py +2 -5
  229. wbportfolio/viewsets/adjustments.py +1 -0
  230. wbportfolio/viewsets/assets.py +62 -51
  231. wbportfolio/viewsets/assets_and_net_new_money_progression.py +1 -0
  232. wbportfolio/viewsets/charts/assets.py +3 -1
  233. wbportfolio/viewsets/configs/buttons/__init__.py +1 -1
  234. wbportfolio/viewsets/configs/buttons/assets.py +1 -0
  235. wbportfolio/viewsets/configs/buttons/custodians.py +1 -0
  236. wbportfolio/viewsets/configs/buttons/mixins.py +1 -20
  237. wbportfolio/viewsets/configs/buttons/portfolios.py +90 -76
  238. wbportfolio/viewsets/configs/buttons/signals.py +1 -0
  239. wbportfolio/viewsets/configs/buttons/trades.py +1 -0
  240. wbportfolio/viewsets/configs/display/__init__.py +2 -1
  241. wbportfolio/viewsets/configs/display/adjustments.py +1 -0
  242. wbportfolio/viewsets/configs/display/assets.py +7 -6
  243. wbportfolio/viewsets/configs/display/claim.py +1 -0
  244. wbportfolio/viewsets/configs/display/portfolios.py +127 -79
  245. wbportfolio/viewsets/configs/display/product_performance.py +1 -0
  246. wbportfolio/viewsets/configs/display/rebalancing.py +27 -0
  247. wbportfolio/viewsets/configs/display/trade_proposals.py +7 -4
  248. wbportfolio/viewsets/configs/display/trades.py +75 -42
  249. wbportfolio/viewsets/configs/endpoints/__init__.py +3 -1
  250. wbportfolio/viewsets/configs/endpoints/assets.py +12 -0
  251. wbportfolio/viewsets/configs/endpoints/claim.py +1 -0
  252. wbportfolio/viewsets/configs/endpoints/portfolios.py +23 -7
  253. wbportfolio/viewsets/configs/endpoints/rebalancing.py +6 -0
  254. wbportfolio/viewsets/configs/endpoints/reconciliations.py +1 -0
  255. wbportfolio/viewsets/configs/endpoints/trade_proposals.py +1 -0
  256. wbportfolio/viewsets/configs/endpoints/trades.py +1 -0
  257. wbportfolio/viewsets/configs/menu/adjustments.py +1 -0
  258. wbportfolio/viewsets/configs/menu/assets.py +1 -0
  259. wbportfolio/viewsets/configs/menu/fees.py +1 -0
  260. wbportfolio/viewsets/configs/menu/portfolio_cash_flow.py +1 -0
  261. wbportfolio/viewsets/configs/menu/portfolios.py +4 -2
  262. wbportfolio/viewsets/configs/menu/positions.py +1 -0
  263. wbportfolio/viewsets/configs/menu/roles.py +1 -0
  264. wbportfolio/viewsets/configs/menu/transactions.py +1 -0
  265. wbportfolio/viewsets/configs/previews/portfolios.py +1 -6
  266. wbportfolio/viewsets/configs/titles/__init__.py +1 -1
  267. wbportfolio/viewsets/configs/titles/assets.py +1 -0
  268. wbportfolio/viewsets/configs/titles/fees.py +1 -0
  269. wbportfolio/viewsets/configs/titles/instrument_prices.py +1 -0
  270. wbportfolio/viewsets/configs/titles/portfolios.py +13 -11
  271. wbportfolio/viewsets/configs/titles/roles.py +1 -0
  272. wbportfolio/viewsets/configs/titles/trades.py +1 -0
  273. wbportfolio/viewsets/configs/titles/transactions.py +1 -0
  274. wbportfolio/viewsets/custodians.py +1 -0
  275. wbportfolio/viewsets/esg.py +1 -0
  276. wbportfolio/viewsets/mixins.py +1 -0
  277. wbportfolio/viewsets/portfolio_cash_flow.py +1 -0
  278. wbportfolio/viewsets/portfolio_cash_targets.py +1 -0
  279. wbportfolio/viewsets/portfolio_relationship.py +1 -0
  280. wbportfolio/viewsets/portfolio_swing_pricing.py +1 -0
  281. wbportfolio/viewsets/portfolios.py +228 -61
  282. wbportfolio/viewsets/positions.py +3 -2
  283. wbportfolio/viewsets/product_groups.py +1 -0
  284. wbportfolio/viewsets/product_performance.py +1 -0
  285. wbportfolio/viewsets/products.py +1 -0
  286. wbportfolio/viewsets/reconciliations.py +1 -0
  287. wbportfolio/viewsets/registers.py +1 -0
  288. wbportfolio/viewsets/roles.py +1 -0
  289. wbportfolio/viewsets/signals.py +1 -0
  290. wbportfolio/viewsets/transactions/__init__.py +1 -0
  291. wbportfolio/viewsets/transactions/claim.py +2 -1
  292. wbportfolio/viewsets/transactions/fees.py +1 -0
  293. wbportfolio/viewsets/transactions/mixins.py +1 -0
  294. wbportfolio/viewsets/transactions/rebalancing.py +31 -0
  295. wbportfolio/viewsets/transactions/trade_proposals.py +25 -5
  296. wbportfolio/viewsets/transactions/trades.py +16 -9
  297. wbportfolio/viewsets/transactions/transactions.py +1 -0
  298. {wbportfolio-1.44.5.dist-info → wbportfolio-1.45.1.dist-info}/METADATA +4 -1
  299. wbportfolio-1.45.1.dist-info/RECORD +521 -0
  300. wbportfolio/admin/synchronization/__init__.py +0 -2
  301. wbportfolio/admin/synchronization/admin.py +0 -114
  302. wbportfolio/admin/synchronization/portfolio_synchronization.py +0 -18
  303. wbportfolio/admin/synchronization/price_computation.py +0 -21
  304. wbportfolio/defaults/portfolio/default_rebalancing.py +0 -45
  305. wbportfolio/factories/pytest_utils.py +0 -121
  306. wbportfolio/factories/synchronization.py +0 -40
  307. wbportfolio/models/synchronization/__init__.py +0 -3
  308. wbportfolio/models/synchronization/portfolio_synchronization.py +0 -292
  309. wbportfolio/models/synchronization/price_computation.py +0 -200
  310. wbportfolio/models/synchronization/synchronization.py +0 -188
  311. wbportfolio/serializers/synchronization.py +0 -18
  312. wbportfolio/tests/models/test_synchronization.py +0 -617
  313. wbportfolio/viewsets/synchronization.py +0 -25
  314. wbportfolio-1.44.5.dist-info/RECORD +0 -508
  315. /wbportfolio/{defaults/portfolio → models/graphs}/__init__.py +0 -0
  316. {wbportfolio-1.44.5.dist-info → wbportfolio-1.45.1.dist-info}/WHEEL +0 -0
  317. {wbportfolio-1.44.5.dist-info → wbportfolio-1.45.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,23 +1,27 @@
1
- from datetime import date
1
+ import random
2
+ from datetime import date, timedelta
2
3
  from decimal import Decimal
3
4
  from unittest.mock import patch
4
5
 
6
+ import pandas as pd
5
7
  import pytest
8
+ from django.contrib.contenttypes.models import ContentType
6
9
  from django.db.models import F, Sum
7
10
  from django.forms.models import model_to_dict
8
11
  from faker import Faker
9
12
  from pandas.tseries.offsets import BDay
10
13
  from psycopg.types.range import DateRange
11
14
  from wbcore.contrib.geography.factories import CountryFactory
15
+
12
16
  from wbportfolio.models import (
13
17
  AssetPosition,
14
18
  Portfolio,
15
19
  PortfolioInstrumentPreferredClassificationThroughModel,
16
- PortfolioSynchronization,
20
+ PortfolioPortfolioThroughModel,
21
+ Trade,
17
22
  )
18
- from wbportfolio.pms.typing import Portfolio as PortfolioDTO
19
23
 
20
- from .test_synchronization import callback
24
+ from ...models.portfolio import update_portfolio_after_investable_universe
21
25
  from .utils import PortfolioTestMixin
22
26
 
23
27
  fake = Faker()
@@ -210,10 +214,12 @@ class TestPortfolioModel(PortfolioTestMixin):
210
214
  portfolio = portfolio_factory.create()
211
215
 
212
216
  ind1 = index_factory.create(is_cash=False)
213
- short_underlying_portfolio = ind1.portfolio
217
+ short_underlying_portfolio = portfolio_factory.create()
218
+ ind1.portfolios.add(short_underlying_portfolio)
214
219
 
215
220
  ind2 = index_factory.create(is_cash=False)
216
- long_underlying_portfolio = ind2.portfolio
221
+ long_underlying_portfolio = portfolio_factory.create()
222
+ ind2.portfolios.add(long_underlying_portfolio)
217
223
 
218
224
  asset_position_factory.create(date=weekday, portfolio=portfolio, weighting=-1.0, underlying_instrument=ind1)
219
225
  short_p1 = asset_position_factory.create(
@@ -246,59 +252,52 @@ class TestPortfolioModel(PortfolioTestMixin):
246
252
  )
247
253
  assert Decimal(res.weighting[1]) == pytest.approx((abs(short_p2.weighting)) / total_weight, rel=Decimal(1e-4))
248
254
 
249
- def test_change_at_date(self, asset_position_factory, portfolio, weekday):
255
+ @patch.object(Portfolio, "estimate_net_asset_values", autospec=True)
256
+ @patch("wbportfolio.models.portfolio.compute_metrics_as_task.delay")
257
+ def test_change_at_date(
258
+ self, mock_compute_metrics, mock_estimate_net_asset_values, asset_position_factory, portfolio, weekday
259
+ ):
250
260
  asset_position_factory.create_batch(10, portfolio=portfolio, date=weekday)
251
261
 
252
- portfolio.change_at_date(weekday)
262
+ portfolio.change_at_date(weekday, compute_metrics=True)
263
+
264
+ # test that change at date normalize the weighting
253
265
  total_value = AssetPosition.objects.aggregate(s=Sum("total_value_fx_portfolio"))["s"]
254
266
  for pos in AssetPosition.objects.all():
255
267
  assert float(pos.weighting) == pytest.approx(float(pos.total_value_fx_portfolio / total_value), rel=1e-2)
256
268
 
257
- @patch.object(PortfolioSynchronization, "synchronize_as_task_si")
269
+ mock_estimate_net_asset_values.assert_called_once_with(portfolio, weekday)
270
+ mock_compute_metrics.assert_called_once_with(
271
+ weekday, basket_id=portfolio.id, basket_content_type_id=ContentType.objects.get_for_model(Portfolio).id
272
+ )
273
+
274
+ @patch.object(Portfolio, "get_total_asset_under_management")
275
+ @patch.object(Portfolio, "compute_lookthrough", autospec=True)
258
276
  def test_change_at_date_with_dependent_portfolio(
259
- self, mock_synchronize, asset_position_factory, portfolio_factory, portfolio_synchronization, weekday
277
+ self,
278
+ mock_compute_lookthrough,
279
+ mock_get_total_asset_under_management,
280
+ portfolio_factory,
281
+ product_factory,
282
+ instrument_price_factory,
283
+ customer_trade_factory,
284
+ weekday,
260
285
  ):
261
- mock_synchronize.return_value = callback
262
286
  base_portfolio = portfolio_factory.create()
263
- asset_position_factory.create_batch(10, portfolio=base_portfolio, date=weekday)
287
+ base_portfolio_total_asset_under_management = fake.pydecimal()
288
+ mock_get_total_asset_under_management.return_value = base_portfolio_total_asset_under_management
264
289
 
265
- dependent_portfolio1 = portfolio_factory.create(portfolio_synchronization=portfolio_synchronization)
266
- dependent_portfolio2 = portfolio_factory.create(portfolio_synchronization=portfolio_synchronization)
267
- asset_position_factory.create_batch(10, portfolio=dependent_portfolio1, date=weekday)
268
- asset_position_factory.create_batch(10, portfolio=dependent_portfolio2, date=weekday)
269
-
270
- dependent_portfolio1.depends_on.add(base_portfolio)
271
- dependent_portfolio2.depends_on.add(base_portfolio)
272
- base_portfolio.change_at_date(weekday)
273
-
274
- assert mock_synchronize.call_count == 2
275
-
276
- @patch.object(PortfolioSynchronization, "synchronize_as_task_si")
277
- @pytest.mark.parametrize("portfolio_synchronization__propagate_history", [True])
278
- def test_change_at_date_with_dependent_portfolio_and_history(
279
- self, mock_synchronize, portfolio_synchronization, asset_position_factory, portfolio_factory, weekday
280
- ):
281
- mock_synchronize.return_value = callback
282
- base_portfolio = portfolio_factory.create()
283
- asset_position_factory.create_batch(10, portfolio=base_portfolio, date=weekday)
284
-
285
- dependent_portfolio1 = portfolio_factory.create(portfolio_synchronization=portfolio_synchronization)
286
- dependent_portfolio2 = portfolio_factory.create(portfolio_synchronization=portfolio_synchronization)
287
- asset_position_factory.create_batch(10, portfolio=dependent_portfolio1, date=weekday)
288
- asset_position_factory.create_batch(10, portfolio=dependent_portfolio2, date=weekday)
289
- asset_position_factory.create_batch(10, portfolio=dependent_portfolio1, date=weekday + BDay(6))
290
- asset_position_factory.create_batch(10, portfolio=dependent_portfolio2, date=weekday + BDay(6))
291
- total_nb_days = ((weekday + BDay(6)).date() - weekday).days
292
- dependent_portfolio1.depends_on.add(base_portfolio)
293
- dependent_portfolio2.depends_on.add(base_portfolio)
290
+ dependent_portfolio = portfolio_factory.create(is_lookthrough=True)
291
+ dependent_portfolio.depends_on.add(base_portfolio)
294
292
  base_portfolio.change_at_date(weekday)
295
293
 
296
- assert mock_synchronize.call_count == 2 * (total_nb_days + 1)
294
+ mock_compute_lookthrough.assert_called_once_with(
295
+ dependent_portfolio, weekday, portfolio_total_asset_value=base_portfolio_total_asset_under_management
296
+ )
297
297
 
298
298
  def test_is_active_at_date(
299
299
  self,
300
300
  portfolio,
301
- instrument_factory,
302
301
  ):
303
302
  # a portfolio is active at a date if it is active or the deletion time is greater than that date AND if there is instruments attached, at least one instrument is still active as well
304
303
 
@@ -339,12 +338,15 @@ class TestPortfolioModel(PortfolioTestMixin):
339
338
  next_day = (weekday + BDay(1)).date()
340
339
 
341
340
  i1 = instrument_factory.create(currency=portfolio.currency)
341
+ price1_0 = instrument_price_factory.create(instrument=i1, date=(weekday - BDay(1)).date()) # noqa
342
342
  price1_1 = instrument_price_factory.create(instrument=i1, date=weekday)
343
343
  price1_2 = instrument_price_factory.create(instrument=i1, date=next_day)
344
344
  i2 = instrument_factory.create(currency=portfolio.currency)
345
+ price2_0 = instrument_price_factory.create(instrument=i2, date=(weekday - BDay(1)).date()) # noqa
345
346
  price2_1 = instrument_price_factory.create(instrument=i2, date=weekday)
346
347
  price2_2 = instrument_price_factory.create(instrument=i2, date=next_day)
347
348
  i3 = instrument_factory.create(currency=portfolio.currency)
349
+ price3_0 = instrument_price_factory.create(instrument=i3, date=(weekday - BDay(1)).date()) # noqa
348
350
  price3_1 = instrument_price_factory.create(instrument=i3, date=weekday)
349
351
  price3_2 = instrument_price_factory.create(instrument=i3, date=next_day)
350
352
  i4 = instrument_factory.create(currency=portfolio.currency)
@@ -352,34 +354,34 @@ class TestPortfolioModel(PortfolioTestMixin):
352
354
  a1_1 = asset_position_factory.create(
353
355
  portfolio=portfolio,
354
356
  underlying_instrument=i1,
355
- underlying_instrument_price=price1_1,
357
+ underlying_quote_price=price1_1,
356
358
  date=weekday,
357
359
  weighting=Decimal(0.4),
358
360
  )
359
361
  a2_1 = asset_position_factory.create(
360
362
  portfolio=portfolio,
361
363
  underlying_instrument=i2,
362
- underlying_instrument_price=price2_1,
364
+ underlying_quote_price=price2_1,
363
365
  date=weekday,
364
366
  weighting=Decimal(0.3),
365
367
  )
366
368
  a3_1 = asset_position_factory.create(
367
369
  portfolio=portfolio,
368
370
  underlying_instrument=i3,
369
- underlying_instrument_price=price3_1,
371
+ underlying_quote_price=price3_1,
370
372
  date=weekday,
371
373
  weighting=Decimal(0.2),
372
374
  )
373
375
  a4_1 = asset_position_factory.create( # noqa
374
376
  portfolio=portfolio,
375
377
  underlying_instrument=i4,
376
- underlying_instrument_price=None, # the price won't be created automatically by the fixture, we expect this position to be removed from the propagated portfolio
378
+ underlying_quote_price=None, # the price won't be created automatically by the fixture, we expect this position to be removed from the propagated portfolio
377
379
  date=weekday,
378
380
  weighting=Decimal(0.1),
379
381
  )
380
382
 
381
383
  # Test basic output
382
- portfolio.propagate_or_update_assets(weekday, next_day, delete_existing_assets=False)
384
+ portfolio.propagate_or_update_assets(weekday, next_day)
383
385
  a1_2 = AssetPosition.objects.get(portfolio=portfolio, date=next_day, underlying_instrument=i1)
384
386
  a2_2 = AssetPosition.objects.get(portfolio=portfolio, date=next_day, underlying_instrument=i2)
385
387
  a3_2 = AssetPosition.objects.get(portfolio=portfolio, date=next_day, underlying_instrument=i3)
@@ -397,16 +399,16 @@ class TestPortfolioModel(PortfolioTestMixin):
397
399
  assert a2_2.weighting == pytest.approx(contrib_2 / (contrib_1 + contrib_2 + contrib_3), rel=Decimal(1e4))
398
400
  assert a3_2.weighting == pytest.approx(contrib_3 / (contrib_1 + contrib_2 + contrib_3), rel=Decimal(1e4))
399
401
 
400
- # Test if a deleted assets is kept if delete_existing_assets is set to True
401
- a1_1.delete()
402
- portfolio.propagate_or_update_assets(weekday, next_day, delete_existing_assets=True)
403
- with pytest.raises(AssetPosition.DoesNotExist):
404
- a1_2.refresh_from_db()
402
+ # # Test if a deleted assets is kept if delete_existing_assets is set to True
403
+ # a1_1.delete()
404
+ # portfolio.propagate_or_update_assets(weekday, next_day, delete_existing_assets=True)
405
+ # with pytest.raises(AssetPosition.DoesNotExist):
406
+ # a1_2.refresh_from_db()
405
407
 
406
- a2_2 = AssetPosition.objects.get(portfolio=portfolio, date=next_day, underlying_instrument=i2)
407
- a3_2 = AssetPosition.objects.get(portfolio=portfolio, date=next_day, underlying_instrument=i3)
408
- assert a2_2
409
- assert a3_2
408
+ # a2_2 = AssetPosition.objects.get(portfolio=portfolio, date=next_day, underlying_instrument=i2)
409
+ # a3_2 = AssetPosition.objects.get(portfolio=portfolio, date=next_day, underlying_instrument=i3)
410
+ # assert a2_2
411
+ # assert a3_2
410
412
 
411
413
  # Test that we don't do anything on target portfolio because there is a non estimated position
412
414
  a2_2.is_estimated = False
@@ -421,23 +423,26 @@ class TestPortfolioModel(PortfolioTestMixin):
421
423
  assert a3_2.weighting == a3_2_weighting
422
424
 
423
425
  def test_propagate_or_update_assets_active_states(
424
- self, weekday, active_product, asset_position_factory, instrument
426
+ self, weekday, active_product, asset_position_factory, instrument_price_factory, instrument
425
427
  ):
426
428
  next_day = (weekday + BDay(1)).date()
427
429
 
428
430
  portfolio = active_product.portfolio
431
+ instrument_price_factory.create(date=(weekday - BDay(1)).date(), instrument=instrument)
429
432
 
430
433
  a1 = asset_position_factory.create(
431
434
  portfolio=portfolio, date=weekday, underlying_instrument=instrument, currency=instrument.currency
432
435
  )
433
- asset_position_factory.create(
434
- portfolio=portfolio,
435
- date=next_day,
436
- underlying_instrument=instrument,
437
- currency=instrument.currency,
438
- exchange=a1.exchange,
439
- portfolio_created=a1.portfolio_created,
440
- )
436
+ instrument_price_factory.create(date=next_day, instrument=instrument)
437
+
438
+ # asset_position_factory.create(
439
+ # portfolio=portfolio,
440
+ # date=next_day,
441
+ # underlying_instrument=instrument,
442
+ # currency=instrument.currency,
443
+ # exchange=a1.exchange,
444
+ # portfolio_created=a1.portfolio_created,
445
+ # )
441
446
  active_product.delisted_date = weekday
442
447
  active_product.save()
443
448
  # Test1: test if unactive portfolio keep having the to date assets. (asset found at next day are suppose to be deleted when the portfolio is non active at the from date)
@@ -461,7 +466,7 @@ class TestPortfolioModel(PortfolioTestMixin):
461
466
  a1.initial_shares *= 2
462
467
  a1.save()
463
468
  portfolio.propagate_or_update_assets(weekday, next_day)
464
- a_future.refresh_from_db()
469
+ a_future = AssetPosition.objects.get(portfolio=portfolio, date=next_day)
465
470
  assert a_future.initial_shares == initial_shares * 2
466
471
 
467
472
  # Test that non-estimated shares are not being updated
@@ -484,22 +489,9 @@ class TestPortfolioModel(PortfolioTestMixin):
484
489
  a_future.is_estimated = True
485
490
  a_future.save()
486
491
  portfolio.propagate_or_update_assets(weekday, next_day)
487
- a_future.refresh_from_db()
492
+ a_future = AssetPosition.objects.get(portfolio=portfolio, date=next_day)
488
493
  assert a_future.weighting == 0
489
494
 
490
- @patch.object(PortfolioSynchronization, "synchronize")
491
- @pytest.mark.parametrize("timedelta_days", [fake.pyint(min_value=1, max_value=10)])
492
- def test_resynchronize_history(
493
- self, mock_fct, portfolio, asset_position_factory, weekday, timedelta_days, portfolio_synchronization
494
- ):
495
- portfolio.portfolio_synchronization = portfolio_synchronization
496
- portfolio.save()
497
- asset_position_factory.create(portfolio=portfolio, date=weekday)
498
- portfolio.resynchronize_history(weekday, (weekday + BDay(timedelta_days)).date())
499
- mock_fct.call_count == timedelta_days
500
- with pytest.raises(ValueError):
501
- portfolio.resynchronize_history((weekday + BDay(timedelta_days)).date(), weekday)
502
-
503
495
  def test_update_preferred_classification_per_instrument(
504
496
  self, portfolio, asset_position_factory, equity_factory, classification_factory, classification_group_factory
505
497
  ):
@@ -548,76 +540,7 @@ class TestPortfolioModel(PortfolioTestMixin):
548
540
  res1.refresh_from_db()
549
541
  assert res1
550
542
 
551
- def test_import_positions_at_date(self, portfolio_factory, asset_position_factory, instrument_factory, weekday):
552
- def _serialize(obj):
553
- return {
554
- "portfolio": obj.portfolio.id,
555
- "portfolio_created": obj.portfolio_created.id if obj.portfolio_created else None,
556
- "underlying_instrument": obj.underlying_instrument.id,
557
- "date": obj.date,
558
- "currency": obj.currency.id,
559
- "weighting": obj.weighting,
560
- "initial_currency_fx_rate": obj.initial_currency_fx_rate,
561
- "initial_shares": obj.initial_shares,
562
- "initial_price": obj.initial_price,
563
- "is_estimated": obj.is_estimated,
564
- "exchange": obj.exchange.id if obj.exchange else None,
565
- "asset_valuation_date": obj.asset_valuation_date,
566
- }
567
-
568
- portfolio = portfolio_factory.create()
569
-
570
- i1 = instrument_factory.create()
571
- i2 = instrument_factory.create()
572
- a1 = asset_position_factory.build(
573
- portfolio=portfolio, date=weekday, underlying_instrument=i1, currency=i1.currency, weighting=0.5
574
- )
575
- a2 = asset_position_factory.build(
576
- portfolio=portfolio, date=weekday, underlying_instrument=i2, currency=i2.currency, weighting=0.25
577
- )
578
- a3 = asset_position_factory.build(
579
- portfolio=portfolio,
580
- currency=i2.currency,
581
- underlying_instrument=i2,
582
- initial_price=a2.initial_price,
583
- date=weekday,
584
- weighting=0.25,
585
- )
586
-
587
- portfolio.import_positions_at_date(
588
- PortfolioDTO([a1._build_dto(), a2._build_dto(), a3._build_dto()]),
589
- weekday,
590
- )
591
-
592
- res1 = AssetPosition.objects.get(portfolio=portfolio, date=weekday, underlying_instrument=i1)
593
- res2 = AssetPosition.objects.get(portfolio=portfolio, date=weekday, underlying_instrument=i2)
594
-
595
- assert portfolio.assets.filter(date=weekday).count() == 2
596
-
597
- assert res1.initial_shares == a1.initial_shares
598
- assert res1.weighting == a1.weighting / (a1.weighting + a2.weighting + a3.weighting)
599
- assert res1.initial_currency_fx_rate == a1.initial_currency_fx_rate
600
- assert res1.initial_price == a1.initial_price
601
-
602
- assert res2.initial_shares == a2.initial_shares + a3.initial_shares
603
- assert res2.weighting == a2.weighting + a3.weighting
604
- assert res2.initial_currency_fx_rate == (a2.initial_currency_fx_rate + a3.initial_currency_fx_rate) / 2
605
- assert res2.initial_price == (a2.initial_price + a3.initial_price) / 2
606
-
607
- assert portfolio.assets.filter(date=weekday, underlying_instrument=a2.underlying_instrument).count() == 1
608
- assert not portfolio.assets.filter(
609
- date=weekday, underlying_instrument=a2.underlying_instrument, initial_shares=a3.initial_shares
610
- ).exists()
611
-
612
- portfolio.import_positions_at_date(
613
- PortfolioDTO([a1._build_dto()]),
614
- weekday,
615
- )
616
- res1.refresh_from_db()
617
- with pytest.raises(AssetPosition.DoesNotExist):
618
- res2.refresh_from_db()
619
-
620
- def test_get_total_value(
543
+ def test_get_total_asset_under_management(
621
544
  self, portfolio, customer_trade_factory, instrument_factory, instrument_price_factory, weekday
622
545
  ):
623
546
  i1 = instrument_factory.create()
@@ -647,11 +570,11 @@ class TestPortfolioModel(PortfolioTestMixin):
647
570
  )
648
571
 
649
572
  assert (
650
- portfolio.get_total_value(weekday)
573
+ portfolio.get_total_asset_under_management(weekday)
651
574
  == price11.net_value * (trade_11.shares + trade_12.shares) + price2.net_value * trade_2.shares
652
575
  )
653
- assert portfolio.get_total_value(previous_day) == price12.net_value * trade_12.shares
654
- assert portfolio.get_total_value(previous_day - BDay(1)) == Decimal(0)
576
+ assert portfolio.get_total_asset_under_management(previous_day) == price12.net_value * trade_12.shares
577
+ assert portfolio.get_total_asset_under_management(previous_day - BDay(1)) == Decimal(0)
655
578
 
656
579
  def test_tracked_object(self, portfolio, asset_position_factory):
657
580
  assert not Portfolio.tracked_objects.exists()
@@ -674,3 +597,545 @@ class TestPortfolioModel(PortfolioTestMixin):
674
597
  assert set(Portfolio.objects.filter_invested_at_date(date(2024, 1, 1))) == set()
675
598
  assert set(Portfolio.objects.filter_invested_at_date(date(2024, 1, 2))) == {portfolio}
676
599
  assert set(Portfolio.objects.filter_invested_at_date(date(2024, 1, 1))) == set()
600
+
601
+ def test_compute_lookthrough(
602
+ self,
603
+ active_product,
604
+ weekday,
605
+ portfolio_factory,
606
+ index_factory,
607
+ equity_factory,
608
+ asset_position_factory,
609
+ trade_factory,
610
+ instrument_price_factory,
611
+ instrument_portfolio_through_model_factory,
612
+ ):
613
+ root_index = index_factory.create()
614
+ root_index_portfolio = portfolio_factory.create()
615
+ root_index.portfolios.add(root_index_portfolio)
616
+
617
+ index1 = index_factory.create()
618
+ index1_portfolio = portfolio_factory.create()
619
+ index1.portfolios.add(index1_portfolio)
620
+
621
+ a1 = asset_position_factory.create(
622
+ underlying_instrument=index1,
623
+ portfolio=root_index_portfolio,
624
+ weighting=0.6,
625
+ initial_shares=None,
626
+ initial_price=100,
627
+ date=weekday,
628
+ )
629
+
630
+ index2 = index_factory.create()
631
+ index2_portfolio = portfolio_factory.create()
632
+ index2.portfolios.add(index2_portfolio)
633
+
634
+ a2 = asset_position_factory.create(
635
+ underlying_instrument=index2,
636
+ portfolio=root_index_portfolio,
637
+ weighting=0.4,
638
+ initial_shares=None,
639
+ initial_price=100,
640
+ date=weekday,
641
+ )
642
+
643
+ a1_1 = asset_position_factory.create(
644
+ underlying_instrument=equity_factory.create(),
645
+ portfolio=index1_portfolio,
646
+ weighting=0.2,
647
+ initial_shares=None,
648
+ initial_price=100,
649
+ date=weekday,
650
+ )
651
+ a2_1 = asset_position_factory.create(
652
+ underlying_instrument=equity_factory.create(),
653
+ portfolio=index1_portfolio,
654
+ weighting=0.3,
655
+ initial_shares=None,
656
+ initial_price=100,
657
+ date=weekday,
658
+ )
659
+ a3_1 = asset_position_factory.create(
660
+ underlying_instrument=equity_factory.create(),
661
+ portfolio=index1_portfolio,
662
+ weighting=0.5,
663
+ initial_shares=None,
664
+ initial_price=100,
665
+ date=weekday,
666
+ )
667
+
668
+ a1_2 = asset_position_factory.create(
669
+ underlying_instrument=equity_factory.create(),
670
+ portfolio=index2_portfolio,
671
+ weighting=0.7,
672
+ initial_shares=None,
673
+ initial_price=100,
674
+ date=weekday,
675
+ )
676
+ a2_2 = asset_position_factory.create(
677
+ underlying_instrument=equity_factory.create(),
678
+ portfolio=index2_portfolio,
679
+ weighting=0.3,
680
+ initial_shares=None,
681
+ initial_price=100,
682
+ date=weekday,
683
+ )
684
+
685
+ product_base_portfolio = active_product.primary_portfolio
686
+ product_portfolio = portfolio_factory.create(is_lookthrough=True)
687
+ instrument_portfolio_through_model_factory.create(instrument=active_product, portfolio=product_portfolio)
688
+ trade_factory.create(
689
+ underlying_instrument=active_product,
690
+ transaction_date=weekday,
691
+ transaction_subtype=Trade.Type.SUBSCRIPTION,
692
+ shares=100,
693
+ )
694
+
695
+ product_portfolio.depends_on.add(root_index_portfolio)
696
+
697
+ instrument_portfolio_through_model_factory.create(instrument=active_product, portfolio=product_portfolio)
698
+
699
+ instrument_price_factory.create(instrument=active_product, date=weekday)
700
+ trade_factory.create(
701
+ underlying_instrument=active_product,
702
+ portfolio=product_base_portfolio,
703
+ transaction_date=weekday,
704
+ shares=1000,
705
+ transaction_subtype="SUBSCRIPTION",
706
+ )
707
+
708
+ product_portfolio.compute_lookthrough(weekday)
709
+ assert product_portfolio.assets.filter(date=weekday).count() == 5
710
+ assert float(a1_1.weighting) * float(a1.weighting) == pytest.approx(
711
+ float(
712
+ product_portfolio.assets.filter(
713
+ portfolio_created=index1_portfolio,
714
+ underlying_instrument=a1_1.underlying_instrument,
715
+ date=weekday,
716
+ )
717
+ .first()
718
+ .weighting
719
+ )
720
+ )
721
+ assert float(a2_1.weighting) * float(a1.weighting) == pytest.approx(
722
+ float(
723
+ product_portfolio.assets.filter(
724
+ portfolio_created=index1_portfolio,
725
+ underlying_instrument=a2_1.underlying_instrument,
726
+ date=weekday,
727
+ )
728
+ .first()
729
+ .weighting
730
+ )
731
+ )
732
+ assert float(a3_1.weighting) * float(a1.weighting) == pytest.approx(
733
+ float(
734
+ product_portfolio.assets.filter(
735
+ portfolio_created=index1_portfolio,
736
+ underlying_instrument=a3_1.underlying_instrument,
737
+ date=weekday,
738
+ )
739
+ .first()
740
+ .weighting
741
+ )
742
+ )
743
+ assert float(a1_2.weighting) * float(a2.weighting) == pytest.approx(
744
+ float(
745
+ product_portfolio.assets.filter(
746
+ portfolio_created=index2_portfolio,
747
+ underlying_instrument=a1_2.underlying_instrument,
748
+ date=weekday,
749
+ )
750
+ .first()
751
+ .weighting
752
+ )
753
+ )
754
+ assert float(a2_2.weighting) * float(a2.weighting) == pytest.approx(
755
+ float(
756
+ product_portfolio.assets.filter(
757
+ portfolio_created=index2_portfolio,
758
+ underlying_instrument=a2_2.underlying_instrument,
759
+ date=weekday,
760
+ )
761
+ .first()
762
+ .weighting
763
+ )
764
+ )
765
+ assert Decimal(1.0) == pytest.approx(product_portfolio.assets.aggregate(s=Sum("weighting"))["s"])
766
+
767
+ def test_estimate_net_asset_values(
768
+ self,
769
+ weekday,
770
+ equity_factory,
771
+ product_factory,
772
+ asset_position_factory,
773
+ instrument_price_factory,
774
+ trade_factory,
775
+ ):
776
+ while weekday.weekday() in [5, 6]:
777
+ weekday += timedelta(days=1)
778
+
779
+ previous_sync_date = weekday - timedelta(days=1)
780
+ while previous_sync_date.weekday() in [5, 6]:
781
+ previous_sync_date -= timedelta(days=1)
782
+
783
+ product = product_factory.create(inception_date=weekday - timedelta(days=1), delisted_date=None)
784
+ portfolio = product.portfolio
785
+
786
+ trade_factory.create(
787
+ underlying_instrument=product,
788
+ portfolio=portfolio,
789
+ transaction_date=previous_sync_date,
790
+ shares=1000,
791
+ transaction_subtype="SUBSCRIPTION",
792
+ )
793
+
794
+ e1 = equity_factory.create()
795
+ pe1_1 = instrument_price_factory.create(instrument=e1, date=previous_sync_date)
796
+ pe1_2 = instrument_price_factory.create(instrument=e1, date=weekday)
797
+ e2 = equity_factory.create()
798
+ pe2_1 = instrument_price_factory.create(instrument=e2, date=previous_sync_date)
799
+ pe2_2 = instrument_price_factory.create(instrument=e2, date=weekday)
800
+ e3 = equity_factory.create()
801
+ pe3_1 = instrument_price_factory.create(instrument=e3, date=previous_sync_date)
802
+ pe3_2 = instrument_price_factory.create(instrument=e3, date=weekday)
803
+
804
+ a1_1 = asset_position_factory.create(
805
+ underlying_instrument=e1,
806
+ underlying_quote_price=pe1_1,
807
+ portfolio=portfolio,
808
+ date=previous_sync_date,
809
+ weighting=Decimal(0.3),
810
+ initial_price=Decimal(100),
811
+ initial_shares=300,
812
+ )
813
+ a2_1 = asset_position_factory.create(
814
+ underlying_instrument=e2,
815
+ underlying_quote_price=pe2_1,
816
+ portfolio=portfolio,
817
+ date=previous_sync_date,
818
+ weighting=Decimal(0.5),
819
+ initial_price=Decimal(100),
820
+ initial_shares=500,
821
+ )
822
+ a3_1 = asset_position_factory.create(
823
+ underlying_instrument=e3,
824
+ underlying_quote_price=pe3_1,
825
+ portfolio=portfolio,
826
+ date=previous_sync_date,
827
+ weighting=Decimal(0.2),
828
+ initial_price=Decimal(100),
829
+ initial_shares=200,
830
+ )
831
+
832
+ a1_2 = asset_position_factory.create(
833
+ underlying_instrument=e1,
834
+ underlying_quote_price=pe1_2,
835
+ portfolio=portfolio,
836
+ date=weekday,
837
+ weighting=Decimal(0.3719),
838
+ initial_price=Decimal(150),
839
+ initial_shares=300,
840
+ )
841
+ a2_2 = asset_position_factory.create(
842
+ underlying_instrument=e2,
843
+ underlying_quote_price=pe2_2,
844
+ portfolio=portfolio,
845
+ date=weekday,
846
+ weighting=Decimal(0.4959),
847
+ initial_price=Decimal(120),
848
+ initial_shares=500,
849
+ )
850
+ a3_2 = asset_position_factory.create(
851
+ underlying_instrument=e3,
852
+ underlying_quote_price=pe3_2,
853
+ portfolio=portfolio,
854
+ date=weekday,
855
+ weighting=Decimal(0.1322),
856
+ initial_price=Decimal(80),
857
+ initial_shares=200,
858
+ )
859
+
860
+ price = instrument_price_factory.create(instrument=product, date=previous_sync_date, net_value=100)
861
+ portfolio.estimate_net_asset_values(weekday)
862
+
863
+ total_perf = (
864
+ (a1_2._price / a1_1._price - 1) * a1_1.weighting
865
+ + (a2_2._price / a2_1._price - 1) * a2_1.weighting
866
+ + (a3_2._price / a3_1._price - 1) * a3_1.weighting
867
+ )
868
+ assert product.prices.count() == 2
869
+ assert float(price.net_value * (Decimal(1.0) + total_perf)) == pytest.approx(
870
+ float(product.prices.filter(date=weekday).first().net_value)
871
+ )
872
+
873
+ def test_pms_instrument(self, product_group, index, product, portfolio):
874
+ product_group.portfolios.set([portfolio])
875
+ product.portfolios.set([portfolio])
876
+ index.portfolios.set([portfolio])
877
+ assert set(portfolio.pms_instruments) == {product_group, product, index}
878
+
879
+ @pytest.mark.parametrize(
880
+ "portfolio__is_tracked, portfolio__is_manageable, portfolio__is_lookthrough",
881
+ [
882
+ (True, True, True),
883
+ (True, False, True),
884
+ (True, False, False),
885
+ (False, True, True),
886
+ (False, True, False),
887
+ (False, False, True),
888
+ (False, False, False),
889
+ ],
890
+ )
891
+ def test_cannot_be_rebalanced(self, portfolio):
892
+ assert portfolio.can_be_rebalanced is False
893
+
894
+ @pytest.mark.parametrize(
895
+ "portfolio__is_tracked, portfolio__is_manageable, portfolio__is_lookthrough",
896
+ [
897
+ (True, True, False),
898
+ ],
899
+ )
900
+ def test_can_be_rebalanced(self, portfolio):
901
+ assert portfolio.can_be_rebalanced is True
902
+
903
+ def test_get_analytic_portfolio(
904
+ self, weekday, portfolio, asset_position_factory, instrument_factory, instrument_price_factory
905
+ ):
906
+ i1 = instrument_factory.create()
907
+ i2 = instrument_factory.create()
908
+ p10 = instrument_price_factory.create(instrument=i1, date=(weekday - BDay(1)).date())
909
+ p11 = instrument_price_factory.create(instrument=i1, date=weekday)
910
+ p20 = instrument_price_factory.create(instrument=i2, date=(weekday - BDay(1)).date())
911
+ p21 = instrument_price_factory.create(instrument=i2, date=weekday)
912
+
913
+ a1 = asset_position_factory.create(date=weekday, portfolio=portfolio, underlying_instrument=i1)
914
+ a1.refresh_from_db()
915
+ a2 = asset_position_factory.create(
916
+ date=weekday, portfolio=portfolio, underlying_instrument=i2, weighting=Decimal(1.0) - a1.weighting
917
+ )
918
+ a2.refresh_from_db()
919
+
920
+ analytic_portfolio = portfolio.get_analytic_portfolio(weekday)
921
+ assert analytic_portfolio.weights.tolist() == [float(a1.weighting), float(a2.weighting)]
922
+ expected_X = pd.DataFrame(
923
+ [[float(p11.net_value / p10.net_value - Decimal(1)), float(p21.net_value / p20.net_value - Decimal(1))]],
924
+ columns=[i1.id, i2.id],
925
+ index=[weekday],
926
+ )
927
+ expected_X.index = pd.to_datetime(expected_X.index)
928
+ pd.testing.assert_frame_equal(analytic_portfolio.X, expected_X, check_names=False, check_freq=False)
929
+
930
+ def test_get_total_asset_value(self, weekday, portfolio, asset_position_factory):
931
+ a1 = asset_position_factory.create(date=weekday, portfolio=portfolio)
932
+ a2 = asset_position_factory.create(date=weekday, portfolio=portfolio)
933
+ a3 = asset_position_factory.create(date=weekday, portfolio=portfolio)
934
+ assert (
935
+ portfolio.get_total_asset_value(weekday)
936
+ == a1.initial_price * a1.initial_shares * a1.initial_currency_fx_rate
937
+ + a2.initial_price * a2.initial_shares * a2.initial_currency_fx_rate
938
+ + a3.initial_price * a3.initial_shares * a3.initial_currency_fx_rate
939
+ )
940
+
941
+ @pytest.mark.parametrize("name", fake.word())
942
+ def test_create_model_portfolio(self, name, currency):
943
+ portfolio = Portfolio.create_model_portfolio(name, currency)
944
+ assert portfolio.name == name
945
+ assert portfolio.currency == currency
946
+ assert portfolio.is_manageable is True
947
+ pms_instruments = list(portfolio.pms_instruments)
948
+ assert len(pms_instruments) == 1
949
+ assert pms_instruments[0].instrument_type.key == "index"
950
+ assert pms_instruments[0].name == name
951
+ assert pms_instruments[0].currency == currency
952
+
953
+ @patch.object(Portfolio, "propagate_or_update_assets", autospec=True)
954
+ def test_update_portfolio_after_investable_universe(
955
+ self, mock_fct, weekday, portfolio_factory, asset_position_factory
956
+ ):
957
+ untracked_portfolio = portfolio_factory.create(is_tracked=False) # noqa
958
+ asset_position_factory.create(portfolio=untracked_portfolio)
959
+ tracked_lookthrough_portfolio = portfolio_factory.create(is_tracked=True, is_lookthrough=True) # noqa
960
+ asset_position_factory.create(portfolio=tracked_lookthrough_portfolio)
961
+
962
+ update_portfolio_after_investable_universe(end_date=weekday)
963
+ mock_fct.assert_not_called()
964
+
965
+ tracked_portfolio = portfolio_factory.create(is_tracked=True)
966
+ asset_position_factory.create(portfolio=tracked_portfolio)
967
+
968
+ update_portfolio_after_investable_universe(end_date=weekday)
969
+ mock_fct.assert_called_once_with(tracked_portfolio, (weekday - BDay(1)).date(), weekday)
970
+
971
+ def test_get_weights(
972
+ self, weekday, portfolio_factory, asset_position_factory, instrument_factory, instrument_price_factory
973
+ ):
974
+ portfolio = portfolio_factory.create()
975
+ portfolio_created = portfolio_factory.create()
976
+
977
+ a1 = asset_position_factory.create(date=weekday, portfolio=portfolio)
978
+ a2 = asset_position_factory.create(date=weekday, portfolio=portfolio)
979
+ a3 = asset_position_factory.create(
980
+ date=weekday,
981
+ portfolio=portfolio,
982
+ underlying_instrument=a2.underlying_instrument,
983
+ portfolio_created=portfolio_created,
984
+ )
985
+ a1.refresh_from_db()
986
+ a2.refresh_from_db()
987
+ a3.refresh_from_db()
988
+ weights = portfolio.get_weights(weekday)
989
+ assert weights[a1.underlying_quote.id] == float(a1.weighting)
990
+ assert weights[a2.underlying_quote.id] == float(a2.weighting + a3.weighting)
991
+
992
+ def test_get_estimated_portfolio_from_weights(
993
+ self, weekday, portfolio, instrument, instrument_price_factory, currency_fx_rates_factory
994
+ ):
995
+ p = instrument_price_factory.create(instrument=instrument, date=weekday)
996
+ fx_portfolio = currency_fx_rates_factory.create(currency=portfolio.currency, date=weekday)
997
+ fx_instrument = currency_fx_rates_factory.create(currency=instrument.currency, date=weekday)
998
+
999
+ weights = {instrument.id: random.random()}
1000
+ prices = {}
1001
+
1002
+ res = list(portfolio.get_estimated_portfolio_from_weights(weekday, weights, prices))
1003
+ a = res[0]
1004
+ assert len(res) == 1
1005
+ assert a.date == weekday
1006
+ assert a.underlying_quote == instrument
1007
+ assert a.underlying_quote_price == p
1008
+ assert a.initial_price == p.net_value
1009
+ assert a.weighting == weights[instrument.id]
1010
+ assert a.currency_fx_rate_portfolio_to_usd == fx_portfolio
1011
+ assert a.currency_fx_rate_instrument_to_usd == fx_instrument
1012
+
1013
+ # ensure saving the unsave assetposition do not lead to exception
1014
+ a.save()
1015
+ assert a
1016
+
1017
+ def test_batch_portfolio_with_rebalancer(
1018
+ self, weekday, rebalancer_factory, portfolio, asset_position_factory, instrument_price_factory
1019
+ ):
1020
+ middle_date = (weekday + BDay(1)).date()
1021
+ rebalancing_date = (middle_date + BDay(1)).date()
1022
+
1023
+ a1 = asset_position_factory.create(date=weekday, portfolio=portfolio, weighting=0.7)
1024
+ a2 = asset_position_factory.create(date=weekday, portfolio=portfolio, weighting=0.3)
1025
+
1026
+ instrument_price_factory.create(instrument=a1.underlying_instrument, date=(weekday - BDay(1)).date())
1027
+ instrument_price_factory.create(instrument=a1.underlying_instrument, date=weekday)
1028
+ instrument_price_factory.create(instrument=a1.underlying_instrument, date=middle_date)
1029
+ instrument_price_factory.create(instrument=a1.underlying_instrument, date=rebalancing_date)
1030
+ instrument_price_factory.create(instrument=a2.underlying_instrument, date=(weekday - BDay(1)).date())
1031
+ instrument_price_factory.create(instrument=a2.underlying_instrument, date=weekday)
1032
+ instrument_price_factory.create(instrument=a2.underlying_instrument, date=middle_date)
1033
+ instrument_price_factory.create(instrument=a2.underlying_instrument, date=rebalancing_date)
1034
+
1035
+ rebalancer_factory.create(portfolio=portfolio, frequency="RRULE:FREQ=DAILY;", activation_date=rebalancing_date)
1036
+ rebalancing_trade_proposal = portfolio.batch_portfolio(weekday, rebalancing_date)
1037
+
1038
+ # check that the position before the rebalancing date were created
1039
+ assert portfolio.assets.get(date=middle_date, underlying_instrument=a1.underlying_instrument)
1040
+ assert portfolio.assets.get(date=middle_date, underlying_instrument=a2.underlying_instrument)
1041
+ with pytest.raises(
1042
+ AssetPosition.DoesNotExist
1043
+ ): # there is no asset position because the rebalancing stopped it:
1044
+ portfolio.assets.get(date=rebalancing_date)
1045
+
1046
+ # we expect a equally rebalancing (default) so both trades needs to be created
1047
+ t1 = rebalancing_trade_proposal.trades.get(
1048
+ transaction_date=rebalancing_date, underlying_instrument=a1.underlying_instrument
1049
+ )
1050
+ t2 = rebalancing_trade_proposal.trades.get(
1051
+ transaction_date=rebalancing_date, underlying_instrument=a2.underlying_instrument
1052
+ )
1053
+ assert t1._target_weight == Decimal("0.5")
1054
+ assert t2._target_weight == Decimal("0.5")
1055
+
1056
+ # we approve the rebalancing trade proposal
1057
+ assert rebalancing_trade_proposal.status == "SUBMIT"
1058
+ rebalancing_trade_proposal.approve()
1059
+ rebalancing_trade_proposal.save()
1060
+
1061
+ # check that the rebalancing was applied and position reflect that
1062
+ assert portfolio.assets.get(
1063
+ date=rebalancing_date, underlying_instrument=a1.underlying_instrument
1064
+ ).weighting == Decimal("0.5")
1065
+ assert portfolio.assets.get(
1066
+ date=rebalancing_date, underlying_instrument=a2.underlying_instrument
1067
+ ).weighting == Decimal("0.5")
1068
+
1069
+ def test_bulk_create_positions(self, portfolio, weekday, asset_position_factory, instrument_factory):
1070
+ portfolio.is_manageable = False
1071
+ portfolio.save()
1072
+ i1 = instrument_factory.create()
1073
+ i2 = instrument_factory.create()
1074
+ i3 = instrument_factory.create()
1075
+ a1 = asset_position_factory.build(date=weekday, portfolio=portfolio, underlying_instrument=i1)
1076
+
1077
+ # check initial creation
1078
+ portfolio.bulk_create_positions([a1])
1079
+ assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).weighting == a1.weighting
1080
+ assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).underlying_instrument == i1
1081
+
1082
+ # check that if we change key value, an already exising position will be updated accordingly
1083
+ a1.weighting = Decimal(0.5)
1084
+ portfolio.bulk_create_positions([a1])
1085
+ assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).weighting == Decimal(0.5)
1086
+
1087
+ a2 = asset_position_factory.build(date=weekday, portfolio=portfolio, underlying_instrument=i2)
1088
+ portfolio.bulk_create_positions([a2])
1089
+ assert (
1090
+ AssetPosition.objects.get(portfolio=portfolio, date=weekday, underlying_instrument=i1).weighting
1091
+ == a1.weighting
1092
+ )
1093
+ assert (
1094
+ AssetPosition.objects.get(portfolio=portfolio, date=weekday, underlying_instrument=i2).weighting
1095
+ == a2.weighting
1096
+ )
1097
+
1098
+ a3 = asset_position_factory.build(date=weekday, portfolio=portfolio, underlying_instrument=i3)
1099
+ portfolio.bulk_create_positions([a3], delete_leftovers=True)
1100
+ assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).weighting == a3.weighting
1101
+ assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).underlying_instrument == i3
1102
+
1103
+ def test_to_dependency_iterator(self, portfolio_factory, asset_position_factory, index, weekday):
1104
+ Portfolio.objects.all().delete() # ensure no portfolio remains
1105
+ dependant_portfolio = portfolio_factory.create(name="dependant portfolio", id=1)
1106
+ dependency_portfolio = portfolio_factory.create(name="dependency portfolio", id=2)
1107
+ PortfolioPortfolioThroughModel.objects.create(
1108
+ portfolio=dependant_portfolio, dependency_portfolio=dependency_portfolio
1109
+ )
1110
+ index_portfolio = portfolio_factory.create(name="Index portfolio", id=3)
1111
+ index.portfolios.add(index_portfolio)
1112
+ asset_position_factory.create(portfolio=dependency_portfolio, underlying_instrument=index, date=weekday)
1113
+
1114
+ undependant_portfolio = portfolio_factory.create(name="undependant portfolio", id=4)
1115
+ res = list(Portfolio.objects.all().to_dependency_iterator(weekday))
1116
+ assert res == [index_portfolio, dependency_portfolio, dependant_portfolio, undependant_portfolio]
1117
+
1118
+ def test_get_returns(self, instrument_factory, instrument_price_factory, portfolio):
1119
+ v1 = date(2025, 1, 1)
1120
+ v2 = date(2025, 1, 2)
1121
+ v3 = date(2025, 1, 3)
1122
+
1123
+ i1 = instrument_factory.create()
1124
+ i2 = instrument_factory.create()
1125
+
1126
+ i11 = instrument_price_factory.create(date=v1, instrument=i1)
1127
+ i12 = instrument_price_factory.create(date=v2, instrument=i1)
1128
+ i13 = instrument_price_factory.create(date=v3, instrument=i1)
1129
+ i11.refresh_from_db()
1130
+ i12.refresh_from_db()
1131
+ i13.refresh_from_db()
1132
+ returns, _ = portfolio.get_returns([i1.id, i2.id], from_date=v1, to_date=v3)
1133
+
1134
+ expected_returns = pd.DataFrame(
1135
+ [[i12.net_value / i11.net_value - 1], [i13.net_value / i12.net_value - 1]],
1136
+ index=[v2, v3],
1137
+ columns=[i1.id],
1138
+ dtype="float64",
1139
+ )
1140
+ expected_returns.index = pd.to_datetime(expected_returns.index)
1141
+ pd.testing.assert_frame_equal(returns, expected_returns, check_names=False, check_freq=False, atol=1e-6)