wbportfolio 1.44.5__py2.py3-none-any.whl → 1.45.0__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 (315) 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 +20 -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 +144 -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 +1 -0
  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 +1 -0
  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 +594 -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/claim.py +1 -0
  251. wbportfolio/viewsets/configs/endpoints/portfolios.py +23 -7
  252. wbportfolio/viewsets/configs/endpoints/rebalancing.py +6 -0
  253. wbportfolio/viewsets/configs/endpoints/reconciliations.py +1 -0
  254. wbportfolio/viewsets/configs/endpoints/trade_proposals.py +1 -0
  255. wbportfolio/viewsets/configs/endpoints/trades.py +1 -0
  256. wbportfolio/viewsets/configs/menu/adjustments.py +1 -0
  257. wbportfolio/viewsets/configs/menu/assets.py +1 -0
  258. wbportfolio/viewsets/configs/menu/fees.py +1 -0
  259. wbportfolio/viewsets/configs/menu/portfolio_cash_flow.py +1 -0
  260. wbportfolio/viewsets/configs/menu/portfolios.py +4 -2
  261. wbportfolio/viewsets/configs/menu/positions.py +1 -0
  262. wbportfolio/viewsets/configs/menu/roles.py +1 -0
  263. wbportfolio/viewsets/configs/menu/transactions.py +1 -0
  264. wbportfolio/viewsets/configs/previews/portfolios.py +1 -6
  265. wbportfolio/viewsets/configs/titles/__init__.py +1 -1
  266. wbportfolio/viewsets/configs/titles/assets.py +1 -0
  267. wbportfolio/viewsets/configs/titles/fees.py +1 -0
  268. wbportfolio/viewsets/configs/titles/instrument_prices.py +1 -0
  269. wbportfolio/viewsets/configs/titles/portfolios.py +13 -11
  270. wbportfolio/viewsets/configs/titles/roles.py +1 -0
  271. wbportfolio/viewsets/configs/titles/trades.py +1 -0
  272. wbportfolio/viewsets/configs/titles/transactions.py +1 -0
  273. wbportfolio/viewsets/custodians.py +1 -0
  274. wbportfolio/viewsets/esg.py +1 -0
  275. wbportfolio/viewsets/mixins.py +1 -0
  276. wbportfolio/viewsets/portfolio_cash_flow.py +1 -0
  277. wbportfolio/viewsets/portfolio_cash_targets.py +1 -0
  278. wbportfolio/viewsets/portfolio_relationship.py +1 -0
  279. wbportfolio/viewsets/portfolio_swing_pricing.py +1 -0
  280. wbportfolio/viewsets/portfolios.py +228 -61
  281. wbportfolio/viewsets/positions.py +3 -2
  282. wbportfolio/viewsets/product_groups.py +1 -0
  283. wbportfolio/viewsets/product_performance.py +1 -0
  284. wbportfolio/viewsets/products.py +1 -0
  285. wbportfolio/viewsets/reconciliations.py +1 -0
  286. wbportfolio/viewsets/registers.py +1 -0
  287. wbportfolio/viewsets/roles.py +1 -0
  288. wbportfolio/viewsets/signals.py +1 -0
  289. wbportfolio/viewsets/transactions/__init__.py +1 -0
  290. wbportfolio/viewsets/transactions/claim.py +2 -1
  291. wbportfolio/viewsets/transactions/fees.py +1 -0
  292. wbportfolio/viewsets/transactions/mixins.py +1 -0
  293. wbportfolio/viewsets/transactions/rebalancing.py +31 -0
  294. wbportfolio/viewsets/transactions/trade_proposals.py +25 -5
  295. wbportfolio/viewsets/transactions/trades.py +16 -9
  296. wbportfolio/viewsets/transactions/transactions.py +1 -0
  297. {wbportfolio-1.44.5.dist-info → wbportfolio-1.45.0.dist-info}/METADATA +4 -1
  298. {wbportfolio-1.44.5.dist-info → wbportfolio-1.45.0.dist-info}/RECORD +301 -288
  299. wbportfolio/admin/synchronization/__init__.py +0 -2
  300. wbportfolio/admin/synchronization/admin.py +0 -114
  301. wbportfolio/admin/synchronization/portfolio_synchronization.py +0 -18
  302. wbportfolio/admin/synchronization/price_computation.py +0 -21
  303. wbportfolio/defaults/portfolio/default_rebalancing.py +0 -45
  304. wbportfolio/factories/pytest_utils.py +0 -121
  305. wbportfolio/factories/synchronization.py +0 -40
  306. wbportfolio/models/synchronization/__init__.py +0 -3
  307. wbportfolio/models/synchronization/portfolio_synchronization.py +0 -292
  308. wbportfolio/models/synchronization/price_computation.py +0 -200
  309. wbportfolio/models/synchronization/synchronization.py +0 -188
  310. wbportfolio/serializers/synchronization.py +0 -18
  311. wbportfolio/tests/models/test_synchronization.py +0 -617
  312. wbportfolio/viewsets/synchronization.py +0 -25
  313. /wbportfolio/{defaults/portfolio → models/graphs}/__init__.py +0 -0
  314. {wbportfolio-1.44.5.dist-info → wbportfolio-1.45.0.dist-info}/WHEEL +0 -0
  315. {wbportfolio-1.44.5.dist-info → wbportfolio-1.45.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,3 +1,4 @@
1
+ from contextlib import suppress
1
2
  from datetime import date
2
3
  from decimal import Decimal
3
4
 
@@ -30,6 +31,7 @@ from wbcore.utils.enum import ChoiceEnum
30
31
  from wbfdm.models import Classification, ClassificationGroup, Instrument
31
32
  from wbfdm.models.instruments.instrument_prices import InstrumentPrice
32
33
  from wbfdm.signals import add_instrument_to_investable_universe
34
+
33
35
  from wbportfolio.import_export.handlers.asset_position import AssetPositionImportHandler
34
36
  from wbportfolio.models.portfolio_relationship import (
35
37
  InstrumentPortfolioThroughModel,
@@ -97,31 +99,31 @@ class DefaultAssetPositionManager(models.Manager):
97
99
  F("applied_adjustment__cumulative_factor") * F("applied_adjustment__factor"), Decimal(1.0)
98
100
  ),
99
101
  shares=F("initial_shares") / F("adjusting_factor"),
100
- price=Coalesce(F("underlying_instrument_price__net_value"), F("initial_price")),
102
+ price=Coalesce(F("underlying_quote_price__net_value"), F("initial_price")),
101
103
  market_capitalization=ExpressionWrapper(
102
104
  Coalesce(
103
- F("underlying_instrument_price__market_capitalization_consolidated"),
104
- F("underlying_instrument_price__market_capitalization"),
105
+ F("underlying_quote_price__market_capitalization_consolidated"),
106
+ F("underlying_quote_price__market_capitalization"),
105
107
  ),
106
108
  output_field=models.DecimalField(),
107
109
  ),
108
- beta=ExpressionWrapper(F("underlying_instrument_price__beta"), output_field=models.DecimalField()),
110
+ beta=ExpressionWrapper(F("underlying_quote_price__beta"), output_field=models.DecimalField()),
109
111
  correlation=ExpressionWrapper(
110
- F("underlying_instrument_price__correlation"), output_field=models.DecimalField()
112
+ F("underlying_quote_price__correlation"), output_field=models.DecimalField()
111
113
  ),
112
114
  sharpe_ratio=ExpressionWrapper(
113
- F("underlying_instrument_price__sharpe_ratio"), output_field=models.DecimalField()
115
+ F("underlying_quote_price__sharpe_ratio"), output_field=models.DecimalField()
114
116
  ),
115
117
  volume=Coalesce(
116
- ExpressionWrapper(F("underlying_instrument_price__volume"), output_field=models.DecimalField()),
118
+ ExpressionWrapper(F("underlying_quote_price__volume"), output_field=models.DecimalField()),
117
119
  Decimal(0),
118
120
  ),
119
121
  volume_50d=Coalesce(
120
- ExpressionWrapper(F("underlying_instrument_price__volume_50d"), output_field=models.DecimalField()),
122
+ ExpressionWrapper(F("underlying_quote_price__volume_50d"), output_field=models.DecimalField()),
121
123
  Decimal(0),
122
124
  ),
123
125
  volume_200d=Coalesce(
124
- ExpressionWrapper(F("underlying_instrument_price__volume_200d"), output_field=models.DecimalField()),
126
+ ExpressionWrapper(F("underlying_quote_price__volume_200d"), output_field=models.DecimalField()),
125
127
  Decimal(0),
126
128
  ),
127
129
  currency_fx_rate_instrument_to_usd_rate=Case(
@@ -162,17 +164,6 @@ class DefaultAssetPositionManager(models.Manager):
162
164
  default=Value(False),
163
165
  output_field=models.BooleanField(),
164
166
  ),
165
- underlying_security=Case( # Annotate the parent security if exists
166
- When(underlying_instrument__parent__isnull=False, then=F("underlying_instrument__parent")),
167
- default=F("underlying_instrument"),
168
- ),
169
- underlying_security_instrument_type_key=Case( # Annotate the parent security if exists
170
- When(
171
- underlying_instrument__parent__isnull=False,
172
- then=F("underlying_instrument__parent__instrument_type__key"),
173
- ),
174
- default=F("underlying_instrument__instrument_type__key"),
175
- ),
176
167
  )
177
168
 
178
169
 
@@ -191,7 +182,7 @@ class AnalyticalAssetPositionManager(DefaultAssetPositionManager):
191
182
  previous_price_usd=Subquery(
192
183
  qs_default.filter(
193
184
  date=OuterRef("last_portfolio_date"),
194
- underlying_instrument=OuterRef("underlying_instrument"),
185
+ underlying_quote=OuterRef("underlying_quote"),
195
186
  portfolio=OuterRef("portfolio"),
196
187
  )
197
188
  .order_by("-date")
@@ -373,14 +364,22 @@ class AssetPosition(ImportMixin, models.Model):
373
364
 
374
365
  underlying_instrument = models.ForeignKey(
375
366
  to="wbfdm.Instrument",
376
- related_name="assets",
377
- limit_choices_to=models.Q(children__isnull=True),
367
+ related_name="instrument_assets",
378
368
  on_delete=models.PROTECT,
379
369
  verbose_name="Underlying Instrument",
380
370
  help_text="The instrument that is this asset.",
381
371
  )
382
372
 
383
- underlying_instrument_price = models.ForeignKey(
373
+ underlying_quote = models.ForeignKey(
374
+ to="wbfdm.Instrument",
375
+ related_name="assets",
376
+ limit_choices_to=models.Q(children__isnull=True),
377
+ on_delete=models.PROTECT,
378
+ verbose_name="Underlying Quote",
379
+ help_text="The quote that is this asset.",
380
+ )
381
+
382
+ underlying_quote_price = models.ForeignKey(
384
383
  to="wbfdm.InstrumentPrice",
385
384
  related_name="assets",
386
385
  on_delete=models.SET_NULL,
@@ -394,42 +393,58 @@ class AssetPosition(ImportMixin, models.Model):
394
393
  analytical_objects = AnalyticalAssetPositionManager()
395
394
  unannotated_objects = models.Manager()
396
395
 
397
- def save(self, *args, create_underlying_instrument_price_if_missing: bool = False, **kwargs):
398
- if not getattr(self, "currency", None):
399
- self.currency = self.underlying_instrument.currency
396
+ def pre_save(self, create_underlying_quote_price_if_missing: bool = False):
400
397
  if not self.asset_valuation_date:
401
398
  self.asset_valuation_date = self.date
402
399
 
403
- if not self.underlying_instrument_price:
400
+ if (
401
+ (not hasattr(self, "underlying_instrument") or not self.underlying_instrument)
402
+ and hasattr(self, "underlying_quote")
403
+ and self.underlying_quote
404
+ ):
405
+ self.underlying_instrument = (
406
+ self.underlying_quote.parent if self.underlying_quote.parent else self.underlying_quote
407
+ )
408
+ elif (
409
+ hasattr(self, "underlying_instrument")
410
+ and self.underlying_instrument
411
+ and (not hasattr(self, "underlying_quote") or not self.underlying_quote)
412
+ ):
413
+ try:
414
+ self.underlying_quote = self.underlying_instrument.children.get(is_primary=True)
415
+ except:
416
+ self.underlying_quote = self.underlying_instrument
417
+
418
+ if not getattr(self, "currency", None):
419
+ self.currency = self.underlying_quote.currency
420
+ if not self.underlying_quote_price:
404
421
  try:
405
422
  # We get only the instrument price (and don't create it) because we don't want to create product instrument price on asset position propagation
406
423
  # Instead, we deciced to opt for a post_save based system that will assign the missing position price when a price is created
407
- self.underlying_instrument_price = InstrumentPrice.objects.get(
408
- calculated=False, instrument=self.underlying_instrument, date=self.asset_valuation_date
424
+ self.underlying_quote_price = InstrumentPrice.objects.get(
425
+ calculated=False, instrument=self.underlying_quote, date=self.asset_valuation_date
409
426
  )
410
427
  except InstrumentPrice.DoesNotExist:
411
428
  # if we create instrument price automatically, we need to ensure that the position is not estimated and not from a fake portfolio (e.g. JPM morgan root portfolio)
412
- if create_underlying_instrument_price_if_missing and not self.is_estimated:
429
+ if create_underlying_quote_price_if_missing and not self.is_estimated:
413
430
  net_value = self.initial_price
414
- # in case the position currency and the linked underlying_instrument currency don't correspond, we convert the rate accordingly
415
- if self.currency != self.underlying_instrument.currency:
416
- net_value *= self.currency.convert(
417
- self.asset_valuation_date, self.underlying_instrument.currency
418
- )
419
- self.underlying_instrument_price = InstrumentPrice(
431
+ # in case the position currency and the linked underlying_quote currency don't correspond, we convert the rate accordingly
432
+ if self.currency != self.underlying_quote.currency:
433
+ net_value *= self.currency.convert(self.asset_valuation_date, self.underlying_quote.currency)
434
+ self.underlying_quote_price = InstrumentPrice.objects.create(
420
435
  calculated=False,
421
- instrument=self.underlying_instrument,
436
+ instrument=self.underlying_quote,
422
437
  date=self.asset_valuation_date,
423
438
  net_value=net_value,
424
439
  import_source=self.import_source, # we set the import source to know where this price is coming from
425
440
  )
426
- self.underlying_instrument_price.fill_market_capitalization()
427
- self.underlying_instrument_price.save()
441
+ self.underlying_quote_price.fill_market_capitalization()
442
+ self.underlying_quote_price.save()
428
443
  else: # sometime, the asset valuation date does not correspond to a valid market date. In that case, we get the latest valid instrument price for that product
429
- self.underlying_instrument_price = (
444
+ self.underlying_quote_price = (
430
445
  InstrumentPrice.objects.filter(
431
446
  calculated=False,
432
- instrument=self.underlying_instrument,
447
+ instrument=self.underlying_quote,
433
448
  date__lte=self.asset_valuation_date,
434
449
  )
435
450
  .order_by("date")
@@ -437,14 +452,28 @@ class AssetPosition(ImportMixin, models.Model):
437
452
  )
438
453
 
439
454
  if not self.currency_fx_rate_instrument_to_usd or (self.currency_fx_rate_instrument_to_usd.date != self.date):
440
- rates = CurrencyFXRates.objects.filter(date__lte=self.date, currency=self.underlying_instrument.currency)
441
- if rates.exists():
442
- self.currency_fx_rate_instrument_to_usd = rates.latest("date")
455
+ with suppress(CurrencyFXRates.DoesNotExist):
456
+ self.currency_fx_rate_instrument_to_usd = CurrencyFXRates.objects.get(
457
+ date=self.date, currency=self.underlying_quote.currency
458
+ )
443
459
 
444
460
  if not self.currency_fx_rate_portfolio_to_usd or (self.currency_fx_rate_portfolio_to_usd.date != self.date):
445
- rates = CurrencyFXRates.objects.filter(date__lte=self.date, currency=self.portfolio.currency)
446
- if rates.exists():
447
- self.currency_fx_rate_portfolio_to_usd = rates.latest("date")
461
+ with suppress(CurrencyFXRates.DoesNotExist):
462
+ self.currency_fx_rate_portfolio_to_usd = CurrencyFXRates.objects.get(
463
+ date=self.date, currency=self.portfolio.currency
464
+ )
465
+
466
+ if not self.initial_price and self.underlying_quote_price:
467
+ self.initial_price = self.underlying_quote_price.net_value
468
+ if self.initial_currency_fx_rate is None:
469
+ self.initial_currency_fx_rate = Decimal(1.0)
470
+ if self.currency_fx_rate_portfolio_to_usd and self.currency_fx_rate_instrument_to_usd:
471
+ self.initial_currency_fx_rate = (
472
+ self.currency_fx_rate_portfolio_to_usd.value / self.currency_fx_rate_instrument_to_usd.value
473
+ )
474
+
475
+ def save(self, *args, create_underlying_quote_price_if_missing: bool = False, **kwargs):
476
+ self.pre_save(create_underlying_quote_price_if_missing=create_underlying_quote_price_if_missing)
448
477
  super().save(*args, **kwargs)
449
478
 
450
479
  class Meta:
@@ -460,29 +489,27 @@ class AssetPosition(ImportMixin, models.Model):
460
489
  name="%(app_label)s_%(class)s_weekday_constraint",
461
490
  ),
462
491
  models.UniqueConstraint(
463
- fields=["portfolio", "date", "underlying_instrument", "portfolio_created"],
492
+ fields=["portfolio", "date", "underlying_quote", "portfolio_created"],
464
493
  name="unique_asset_position",
465
494
  nulls_distinct=False,
466
495
  ),
467
496
  ]
468
497
 
469
498
  def __str__(self):
470
- return f"{self.initial_price} - {self.initial_shares} ({self.date}) ({str(self.underlying_instrument)})"
499
+ return f"{self.initial_price} - {self.initial_shares} ({self.date}) ({str(self.underlying_quote)})"
471
500
 
472
501
  def set_weighting(self, new_weighting: Decimal):
473
502
  # Use this method to set the new weighting and ensure that the relative shares are updated accordingly
474
503
  self.weighting = new_weighting
475
504
  if self.initial_shares is not None:
476
505
  if self.weighting == 0 or self.initial_shares == 0:
477
- self.initial_shares = new_weighting * self.get_total_value_portfolio()
506
+ self.initial_shares = new_weighting * self.get_portfolio_total_asset_value()
478
507
  else:
479
508
  self.initial_shares = (new_weighting / self.weighting) * self.initial_shares
480
509
  self.save()
481
510
 
482
- def get_total_value_portfolio(self) -> Decimal:
483
- return self.portfolio.assets.filter(date=self.date).aggregate(s=Sum("total_value_fx_portfolio"))[
484
- "s"
485
- ] or Decimal(0.0)
511
+ def get_portfolio_total_asset_value(self) -> Decimal:
512
+ return self.portfolio.get_total_asset_value(self.date)
486
513
 
487
514
  def _build_dto(self, new_weight: Decimal = None) -> PositionDTO:
488
515
  """
@@ -491,23 +518,23 @@ class AssetPosition(ImportMixin, models.Model):
491
518
  DTO position object
492
519
  """
493
520
  return PositionDTO(
494
- underlying_instrument=self.underlying_instrument.id,
521
+ underlying_instrument=self.underlying_quote.id,
495
522
  weighting=self.weighting if new_weight is None else new_weight,
496
523
  shares=self._shares,
497
524
  date=self.date,
498
525
  asset_valuation_date=self.asset_valuation_date,
499
- instrument_type=self.underlying_instrument.security_instrument_type.id,
500
- currency=self.underlying_instrument.currency.id,
501
- country=self.underlying_instrument.country.id if self.underlying_instrument.country else None,
502
- is_cash=self.underlying_instrument.is_cash,
526
+ instrument_type=self.underlying_quote.instrument_type.id,
527
+ currency=self.underlying_quote.currency.id,
528
+ country=self.underlying_quote.country.id if self.underlying_quote.country else None,
529
+ is_cash=self.underlying_quote.is_cash,
503
530
  primary_classification=(
504
- self.underlying_instrument.primary_classification.id
505
- if self.underlying_instrument.primary_classification
531
+ self.underlying_quote.primary_classification.id
532
+ if self.underlying_quote.primary_classification
506
533
  else None
507
534
  ),
508
535
  favorite_classification=(
509
- self.underlying_instrument.favorite_classification.id
510
- if self.underlying_instrument.favorite_classification
536
+ self.underlying_quote.favorite_classification.id
537
+ if self.underlying_quote.favorite_classification
511
538
  else None
512
539
  ),
513
540
  market_capitalization_usd=self._market_capitalization_usd,
@@ -538,21 +565,17 @@ class AssetPosition(ImportMixin, models.Model):
538
565
  @cached_property
539
566
  @admin.display(description="Market Capitalization")
540
567
  def _market_capitalization(self) -> float:
541
- return (
542
- self.underlying_instrument_price.market_capitalization_consolidated
543
- if self.underlying_instrument_price
544
- else None
545
- )
568
+ return self.underlying_quote_price.market_capitalization_consolidated if self.underlying_quote_price else None
546
569
 
547
570
  @cached_property
548
571
  @admin.display(description="Volume 50d")
549
572
  def _volume_50d(self) -> float:
550
- return self.underlying_instrument_price.volume_50d if self.underlying_instrument_price else None
573
+ return self.underlying_quote_price.volume_50d if self.underlying_quote_price else None
551
574
 
552
575
  @cached_property
553
576
  @admin.display(description="Price (Instrument)")
554
577
  def _price(self) -> Decimal:
555
- return self.underlying_instrument_price.net_value if self.underlying_instrument_price else self.initial_price
578
+ return self.underlying_quote_price.net_value if self.underlying_quote_price else self.initial_price
556
579
 
557
580
  @cached_property
558
581
  @admin.display(description="FX rate")
@@ -575,14 +598,17 @@ class AssetPosition(ImportMixin, models.Model):
575
598
  return self._price * self._shares
576
599
  return Decimal(0)
577
600
 
601
+ @cached_property
602
+ def fx_usd(self) -> Decimal:
603
+ if self.currency_fx_rate_instrument_to_usd:
604
+ return self.currency_fx_rate_instrument_to_usd.value
605
+ return Decimal(1.0)
606
+
578
607
  @cached_property
579
608
  @admin.display(description="Total Value (USD)")
580
609
  def _total_value_fx_usd(self) -> Decimal:
581
- fx_rate = (
582
- self.currency_fx_rate_instrument_to_usd.value if self.currency_fx_rate_instrument_to_usd else Decimal(1.0)
583
- )
584
610
  if self._shares is not None:
585
- return self._price * self._shares * fx_rate
611
+ return self._price * self._shares * self.fx_usd
586
612
  return Decimal(0)
587
613
 
588
614
  @cached_property
@@ -609,23 +635,15 @@ class AssetPosition(ImportMixin, models.Model):
609
635
  @cached_property
610
636
  @admin.display(description="Market Capitalization (USD)")
611
637
  def _market_capitalization_usd(self) -> float:
612
- if self._market_capitalization is not None and self.currency_fx_rate_instrument_to_usd.value is not None:
613
- return self._market_capitalization / float(self.currency_fx_rate_instrument_to_usd.value)
638
+ if self._market_capitalization is not None:
639
+ return self._market_capitalization * float(self.fx_usd)
614
640
  return 0.0
615
641
 
616
642
  @cached_property
617
643
  @admin.display(description="Volume (USD)")
618
644
  def _volume_usd(self) -> float:
619
- if (
620
- self._price_fx_portfolio is not None
621
- and self.currency_fx_rate_instrument_to_usd.value is not None
622
- and self._volume_50d is not None
623
- ):
624
- return (
625
- float(self._price_fx_portfolio)
626
- * float(self.currency_fx_rate_instrument_to_usd.value)
627
- * self._volume_50d
628
- )
645
+ if self._price_fx_portfolio is not None and self._volume_50d is not None:
646
+ return float(self._price_fx_portfolio) * float(self.fx_usd) * self._volume_50d
629
647
  return 0.0
630
648
 
631
649
  @classmethod
@@ -667,8 +685,8 @@ class AssetPosition(ImportMixin, models.Model):
667
685
  qs.annotate(
668
686
  underlying_security_instrument_type_name_repr=Case( # Annotate the parent security if exists
669
687
  When(
670
- underlying_instrument__parent__isnull=False,
671
- then=F("underlying_instrument__parent__instrument_type__name_repr"),
688
+ underlying_instrument__isnull=False,
689
+ then=F("underlying_instrument__instrument_type__name_repr"),
672
690
  ),
673
691
  default=F("underlying_instrument__instrument_type__name_repr"),
674
692
  ),
@@ -824,7 +842,7 @@ class AssetPosition(ImportMixin, models.Model):
824
842
  )
825
843
  return (
826
844
  Instrument.annotated_objects.filter(is_investable_universe=True)
827
- .annotate(has_position=Exists(asset_positions.filter(underlying_instrument=OuterRef("id"))))
845
+ .annotate(has_position=Exists(asset_positions.filter(underlying_quote=OuterRef("id"))))
828
846
  .filter(has_position=True)
829
847
  )
830
848
 
@@ -834,12 +852,9 @@ def post_instrument_price_creation(sender, instance, created, raw, **kwargs):
834
852
  if not raw and created and not instance.calculated:
835
853
  AssetPosition.objects.filter(
836
854
  Q(asset_valuation_date=instance.date)
837
- & Q(underlying_instrument=instance.instrument)
838
- & (
839
- Q(underlying_instrument_price__isnull=True)
840
- | ~Q(asset_valuation_date=F("underlying_instrument_price__date"))
841
- )
842
- ).update(underlying_instrument_price=instance)
855
+ & Q(underlying_quote=instance.instrument)
856
+ & (Q(underlying_quote_price__isnull=True) | ~Q(asset_valuation_date=F("underlying_quote_price__date")))
857
+ ).update(underlying_quote_price=instance)
843
858
 
844
859
 
845
860
  @receiver(pre_merge, sender="wbfdm.Instrument")
@@ -851,7 +866,11 @@ def pre_merge_instrument(sender: models.Model, merged_object: Instrument, main_o
851
866
  new_price=InstrumentPrice.objects.filter(
852
867
  instrument=main_object, date=OuterRef("date"), calculated=False
853
868
  ).values("id")[:1]
854
- ).update(underlying_instrument=main_object, underlying_instrument_price=F("new_price"))
869
+ ).update(
870
+ underlying_quote=main_object,
871
+ underlying_instrument=main_object.parent if main_object.parent else main_object,
872
+ underlying_quote_price=F("new_price"),
873
+ )
855
874
 
856
875
 
857
876
  @receiver(add_instrument_to_investable_universe, sender="wbfdm.Instrument")
@@ -863,7 +882,7 @@ def add_instrument_to_investable_universe(sender: models.Model, **kwargs) -> lis
863
882
  (
864
883
  Instrument.objects.annotate(
865
884
  assets_exists=Exists(
866
- AssetPosition.objects.filter(portfolio__is_tracked=True, underlying_instrument=OuterRef("pk"))
885
+ AssetPosition.objects.filter(portfolio__is_tracked=True, underlying_quote=OuterRef("pk"))
867
886
  )
868
887
  ).filter(Q(assets_exists=True) | Q(portfolios__isnull=False))
869
888
  )
@@ -0,0 +1,144 @@
1
+ import re
2
+ import textwrap
3
+ from datetime import date
4
+
5
+ import networkx as nx
6
+ import plotly.graph_objects as go
7
+ import pydot
8
+ from django.db.models import Q
9
+
10
+ from wbportfolio.models import Portfolio, PortfolioPortfolioThroughModel
11
+
12
+ from .utils import networkx_graph_to_plotly
13
+
14
+
15
+ class PortfolioGraph:
16
+ def __init__(self, portfolio: Portfolio, val_date: date, **graph_kwargs):
17
+ self.graph = pydot.Dot("Portfolio Tree", strict=True, **graph_kwargs)
18
+ self.discovered_portfolios = set()
19
+ self.val_date = val_date
20
+ self._extend_portfolio_graph(portfolio)
21
+
22
+ @classmethod
23
+ def _convert_to_multilines(cls, name: str) -> str:
24
+ lines = textwrap.wrap(name, 30)
25
+ return "\n ".join(lines)
26
+
27
+ def _extend_parent_portfolios_to_graph(self, portfolio):
28
+ for parent_portfolio, weighting in portfolio.get_parent_portfolios(self.val_date):
29
+ if parent_portfolio.assets.filter(date=self.val_date).exists():
30
+ self.graph.add_node(
31
+ pydot.Node(
32
+ str(parent_portfolio.id),
33
+ label=self._convert_to_multilines(str(parent_portfolio)),
34
+ shape="circle",
35
+ orientation="45",
36
+ style="solid",
37
+ )
38
+ )
39
+ self.graph.add_edge(
40
+ pydot.Edge(str(portfolio.id), str(parent_portfolio.id), label=f"{weighting:.2%}", style="dashed")
41
+ )
42
+ # composition_edges.append((str(portfolio.id), str(parent_portfolio.id)))
43
+ self._extend_parent_portfolios_to_graph(parent_portfolio)
44
+ self._extend_portfolio_graph(parent_portfolio)
45
+
46
+ def _extend_child_portfolios_to_graph(self, portfolio):
47
+ for child_portfolio in portfolio.get_child_portfolios(self.val_date):
48
+ self.graph.add_node(
49
+ pydot.Node(
50
+ str(child_portfolio.id),
51
+ label=self._convert_to_multilines(str(child_portfolio)),
52
+ shape="circle",
53
+ orientation="45",
54
+ style="solid",
55
+ )
56
+ )
57
+ # self.graph.add_edge(pydot.Edge(str(child_portfolio.id), str(portfolio.id), label="child", style="dashed"))
58
+ # composition_edges.append((str(child_portfolio.id), str(portfolio.id)))
59
+ self._extend_child_portfolios_to_graph(child_portfolio)
60
+
61
+ def _extend_portfolio_graph(self, portfolio):
62
+ self.graph.add_node(
63
+ pydot.Node(
64
+ str(portfolio.id),
65
+ label=self._convert_to_multilines(str(portfolio)),
66
+ shape="circle",
67
+ orientation="45",
68
+ style="solid",
69
+ )
70
+ )
71
+
72
+ self._extend_parent_portfolios_to_graph(portfolio)
73
+ self._extend_child_portfolios_to_graph(portfolio)
74
+
75
+ # composition_edges = []
76
+ # if composition_edges:
77
+ # with self.graph.subgraph(label=f'composition_{portfolio.id}') as a:
78
+ # a.edges(composition_edges)
79
+ # a.attr(color='blue')
80
+ # a.attr(label='Portfolio Composition')
81
+ self.discovered_portfolios.add(portfolio)
82
+
83
+ for rel in PortfolioPortfolioThroughModel.objects.filter(
84
+ Q(portfolio=portfolio) | Q(dependency_portfolio=portfolio)
85
+ ):
86
+ self.graph.add_node(
87
+ pydot.Node(
88
+ str(rel.portfolio.id),
89
+ label=self._convert_to_multilines(str(rel.portfolio)),
90
+ shape="square",
91
+ orientation="45",
92
+ style="solid",
93
+ )
94
+ )
95
+ self.graph.add_node(
96
+ pydot.Node(
97
+ str(rel.dependency_portfolio.id),
98
+ label=self._convert_to_multilines(str(rel.dependency_portfolio)),
99
+ shape="square",
100
+ orientation="45",
101
+ style="solid",
102
+ )
103
+ )
104
+ label = PortfolioPortfolioThroughModel.Type[rel.type].label
105
+ if rel.dependency_portfolio.is_composition:
106
+ label += " (Composition)"
107
+ self.graph.add_edge(
108
+ pydot.Edge(
109
+ str(rel.dependency_portfolio.id),
110
+ str(rel.portfolio.id),
111
+ label=label,
112
+ style="bold",
113
+ )
114
+ )
115
+ if rel.portfolio.is_lookthrough and rel.type == PortfolioPortfolioThroughModel.Type.PRIMARY:
116
+ self.graph.add_edge(
117
+ pydot.Edge(
118
+ str(rel.portfolio.id), str(rel.dependency_portfolio.id), label="Look-Through", style="dotted"
119
+ )
120
+ )
121
+ if rel.dependency_portfolio not in self.discovered_portfolios:
122
+ self._extend_portfolio_graph(rel.dependency_portfolio)
123
+ if rel.portfolio not in self.discovered_portfolios:
124
+ self._extend_portfolio_graph(rel.portfolio)
125
+
126
+ def to_string(self) -> str:
127
+ return self.graph.to_string()
128
+
129
+ def to_networkx(self) -> nx.Graph:
130
+ return nx.drawing.nx_pydot.from_pydot(self.graph)
131
+
132
+ def to_plotly(self, **kwargs) -> go.Figure:
133
+ node_labels = {
134
+ node.get_name(): node.obj_dict["attributes"].get("label", node.get_name())
135
+ for node in self.graph.get_node_list()
136
+ }
137
+ return networkx_graph_to_plotly(self.to_networkx(), labels=node_labels, **kwargs)
138
+
139
+ def to_svg(self) -> str:
140
+ svg = self.graph.create_svg().decode("utf-8")
141
+ svg_matches = re.findall(r"<svg\b[^<]*(?:(?!<\/svg>)<[^<]*)*<\/svg>", svg, flags=re.DOTALL)
142
+ if svg_matches:
143
+ return svg_matches[0]
144
+ return svg
@@ -0,0 +1,83 @@
1
+ import networkx as nx
2
+ import plotly.graph_objects as go
3
+ from networkx.drawing.nx_agraph import graphviz_layout
4
+
5
+
6
+ def reformat_graph_layout(G, layout):
7
+ """
8
+ this method provide positions based on layout algorithm
9
+ :param G:
10
+ :param layout:
11
+ :return:
12
+ """
13
+ if layout == "graphviz":
14
+ positions = graphviz_layout(G)
15
+ elif layout == "spring":
16
+ positions = nx.fruchterman_reingold_layout(G, k=0.5, iterations=1000)
17
+ elif layout == "spectral":
18
+ positions = nx.spectral_layout(G, scale=0.1)
19
+ elif layout == "random":
20
+ positions = nx.random_layout(G)
21
+ else:
22
+ raise Exception("please specify the layout from graphviz, spring, spectral or random")
23
+
24
+ return positions
25
+
26
+
27
+ def networkx_graph_to_plotly(
28
+ G: nx.Graph,
29
+ labels: dict[str, str] | None = None,
30
+ node_size: int = 10,
31
+ edge_weight: int = 1,
32
+ edge_color: str = "black",
33
+ layout: str = "graphviz",
34
+ title: str = "",
35
+ ) -> go.Figure:
36
+ """
37
+ Visualize a NetworkX graph using Plotly.
38
+ """
39
+ positions = reformat_graph_layout(G, layout)
40
+ if not labels:
41
+ labels = {}
42
+ # Initialize edge traces
43
+ edge_traces = []
44
+ for edge in G.edges():
45
+ x0, y0 = positions[edge[0]]
46
+ x1, y1 = positions[edge[1]]
47
+
48
+ edge_trace = go.Scatter(
49
+ x=[x0, x1], y=[y0, y1], line=dict(width=edge_weight, color=edge_color), hoverinfo="none", mode="lines"
50
+ )
51
+ edge_traces.append(edge_trace)
52
+
53
+ # Initialize node trace
54
+ node_x, node_y, node_colors, node_labels = [], [], [], []
55
+ for node in G.nodes():
56
+ x, y = positions[node]
57
+ node_x.append(x)
58
+ node_y.append(y)
59
+ node_labels.append(labels.get(node, node))
60
+ node_colors.append(len(list(G.neighbors(node)))) # Color based on degree
61
+
62
+ node_trace = go.Scatter(
63
+ x=node_x,
64
+ y=node_y,
65
+ text=node_labels,
66
+ mode="markers+text",
67
+ textfont=dict(family="Calibri (Body)", size=15, color="grey"),
68
+ marker=dict(
69
+ size=node_size,
70
+ color=node_colors,
71
+ ),
72
+ )
73
+
74
+ # Assemble the figure
75
+ fig = go.Figure(
76
+ data=edge_traces + [node_trace],
77
+ layout=go.Layout(
78
+ showlegend=False,
79
+ template="plotly_white",
80
+ margin=dict(l=50, r=50, t=0, b=40),
81
+ ),
82
+ )
83
+ return fig
@@ -1,20 +1,9 @@
1
- from django.db import models
2
1
  from wbfdm.models.instruments import InstrumentType
3
2
 
4
- from .mixins.instruments import PMSInstrument
3
+ from .mixins.instruments import PMSInstrumentAbstractModel
5
4
 
6
5
 
7
- class Index(PMSInstrument):
8
- price_computation = models.ForeignKey(
9
- "wbportfolio.PriceComputation",
10
- null=True,
11
- blank=True,
12
- on_delete=models.SET_NULL,
13
- related_name="indexes",
14
- verbose_name="Price Computation Method",
15
- )
16
- risk_scale = models.PositiveIntegerField(null=True, blank=True, verbose_name="Risk Scale")
17
-
6
+ class Index(PMSInstrumentAbstractModel):
18
7
  def pre_save(self):
19
8
  super().pre_save()
20
9
  self.instrument_type = InstrumentType.INDEX