wbportfolio 1.52.0__py2.py3-none-any.whl → 1.59.4__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 (273) hide show
  1. wbportfolio/admin/__init__.py +3 -1
  2. wbportfolio/admin/indexes.py +1 -1
  3. wbportfolio/admin/orders/__init__.py +2 -0
  4. wbportfolio/admin/orders/order_proposals.py +16 -0
  5. wbportfolio/admin/orders/orders.py +32 -0
  6. wbportfolio/admin/portfolio.py +11 -5
  7. wbportfolio/admin/product_groups.py +1 -1
  8. wbportfolio/admin/products.py +2 -1
  9. wbportfolio/admin/{transactions/rebalancing.py → rebalancing.py} +1 -1
  10. wbportfolio/admin/transactions/__init__.py +0 -2
  11. wbportfolio/admin/transactions/dividends.py +40 -4
  12. wbportfolio/admin/transactions/fees.py +24 -14
  13. wbportfolio/admin/transactions/trades.py +34 -27
  14. wbportfolio/analysis/claims.py +5 -6
  15. wbportfolio/api_clients/ubs.py +162 -0
  16. wbportfolio/constants.py +1 -0
  17. wbportfolio/contrib/company_portfolio/configs/display.py +22 -10
  18. wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
  19. wbportfolio/contrib/company_portfolio/filters.py +10 -10
  20. wbportfolio/contrib/company_portfolio/models.py +69 -39
  21. wbportfolio/contrib/company_portfolio/scripts.py +7 -2
  22. wbportfolio/contrib/company_portfolio/serializers.py +32 -22
  23. wbportfolio/contrib/company_portfolio/tasks.py +12 -1
  24. wbportfolio/contrib/company_portfolio/tests/conftest.py +2 -2
  25. wbportfolio/defaults/fees/default.py +7 -15
  26. wbportfolio/factories/__init__.py +2 -2
  27. wbportfolio/factories/assets.py +1 -1
  28. wbportfolio/factories/dividends.py +8 -3
  29. wbportfolio/factories/fees.py +8 -4
  30. wbportfolio/factories/orders/__init__.py +2 -0
  31. wbportfolio/factories/orders/order_proposals.py +21 -0
  32. wbportfolio/factories/orders/orders.py +34 -0
  33. wbportfolio/factories/portfolios.py +2 -1
  34. wbportfolio/factories/product_groups.py +3 -3
  35. wbportfolio/factories/products.py +3 -3
  36. wbportfolio/factories/rebalancing.py +1 -1
  37. wbportfolio/factories/trades.py +12 -16
  38. wbportfolio/filters/assets.py +18 -4
  39. wbportfolio/filters/orders/__init__.py +2 -0
  40. wbportfolio/filters/orders/order_proposals.py +55 -0
  41. wbportfolio/filters/orders/orders.py +11 -0
  42. wbportfolio/filters/portfolios.py +38 -1
  43. wbportfolio/filters/positions.py +0 -1
  44. wbportfolio/filters/transactions/__init__.py +1 -2
  45. wbportfolio/filters/transactions/fees.py +5 -12
  46. wbportfolio/filters/transactions/trades.py +16 -8
  47. wbportfolio/filters/transactions/utils.py +42 -0
  48. wbportfolio/import_export/backends/ubs/__init__.py +1 -0
  49. wbportfolio/import_export/backends/ubs/asset_position.py +6 -7
  50. wbportfolio/import_export/backends/ubs/fees.py +10 -20
  51. wbportfolio/import_export/backends/ubs/instrument_price.py +6 -6
  52. wbportfolio/import_export/backends/ubs/trade.py +48 -0
  53. wbportfolio/import_export/backends/utils.py +0 -17
  54. wbportfolio/import_export/handlers/asset_position.py +22 -10
  55. wbportfolio/import_export/handlers/dividend.py +8 -8
  56. wbportfolio/import_export/handlers/fees.py +13 -23
  57. wbportfolio/import_export/handlers/orders.py +71 -0
  58. wbportfolio/import_export/handlers/trade.py +53 -77
  59. wbportfolio/import_export/parsers/default_mapping.py +1 -1
  60. wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +2 -2
  61. wbportfolio/import_export/parsers/jpmorgan/fees.py +4 -4
  62. wbportfolio/import_export/parsers/jpmorgan/strategy.py +59 -85
  63. wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
  64. wbportfolio/import_export/parsers/leonteq/customer_trade.py +5 -5
  65. wbportfolio/import_export/parsers/leonteq/fees.py +11 -7
  66. wbportfolio/import_export/parsers/leonteq/trade.py +2 -6
  67. wbportfolio/import_export/parsers/natixis/d1_fees.py +2 -2
  68. wbportfolio/import_export/parsers/natixis/dividend.py +4 -9
  69. wbportfolio/import_export/parsers/natixis/equity.py +22 -4
  70. wbportfolio/import_export/parsers/natixis/fees.py +7 -9
  71. wbportfolio/import_export/parsers/natixis/utils.py +13 -19
  72. wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +1 -1
  73. wbportfolio/import_export/parsers/sg_lux/equity.py +10 -10
  74. wbportfolio/import_export/parsers/sg_lux/fees.py +2 -2
  75. wbportfolio/import_export/parsers/sg_lux/perf_fees.py +2 -2
  76. wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
  77. wbportfolio/import_export/parsers/sg_lux/utils.py +2 -2
  78. wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
  79. wbportfolio/import_export/parsers/societe_generale/strategy.py +5 -5
  80. wbportfolio/import_export/parsers/tellco/customer_trade.py +2 -1
  81. wbportfolio/import_export/parsers/tellco/valuation.py +4 -3
  82. wbportfolio/import_export/parsers/ubs/api/fees.py +2 -2
  83. wbportfolio/import_export/parsers/ubs/api/trade.py +39 -0
  84. wbportfolio/import_export/parsers/ubs/customer_trade.py +7 -5
  85. wbportfolio/import_export/parsers/ubs/equity.py +3 -2
  86. wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
  87. wbportfolio/import_export/parsers/vontobel/customer_trade.py +2 -3
  88. wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +0 -1
  89. wbportfolio/import_export/parsers/vontobel/management_fees.py +12 -20
  90. wbportfolio/import_export/parsers/vontobel/performance_fees.py +5 -8
  91. wbportfolio/import_export/parsers/vontobel/valuation_api.py +4 -1
  92. wbportfolio/import_export/resources/trades.py +3 -3
  93. wbportfolio/import_export/utils.py +3 -1
  94. wbportfolio/jinja2/wbportfolio/sql/aum_nnm.sql +2 -2
  95. wbportfolio/metric/backends/base.py +2 -2
  96. wbportfolio/migrations/0059_fees_unique_fees.py +1 -1
  97. wbportfolio/migrations/0077_remove_transaction_currency_and_more.py +622 -0
  98. wbportfolio/migrations/0078_trade_drift_factor.py +26 -0
  99. wbportfolio/migrations/0079_alter_trade_drift_factor.py +19 -0
  100. wbportfolio/migrations/0080_alter_trade_drift_factor_alter_trade_weighting.py +19 -0
  101. wbportfolio/migrations/0081_alter_trade_drift_factor.py +19 -0
  102. wbportfolio/migrations/0082_remove_tradeproposal_creator_and_more.py +93 -0
  103. wbportfolio/migrations/0083_order_alter_trade_options_and_more.py +181 -0
  104. wbportfolio/migrations/0084_orderproposal_min_order_value.py +25 -0
  105. wbportfolio/migrations/0085_order_desired_target_weight.py +26 -0
  106. wbportfolio/migrations/0086_orderproposal_total_cash_weight.py +19 -0
  107. wbportfolio/migrations/0087_product_order_routing_custodian_adapter.py +94 -0
  108. wbportfolio/migrations/0088_orderproposal_total_effective_portfolio_contribution.py +19 -0
  109. wbportfolio/migrations/0089_orderproposal_min_weighting.py +71 -0
  110. wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
  111. wbportfolio/migrations/0091_remove_order_execution_confirmed_and_more.py +32 -0
  112. wbportfolio/migrations/0092_order_quantization_error_alter_orderproposal_status.py +49 -0
  113. wbportfolio/migrations/0093_remove_portfolioportfoliothroughmodel_unique_primary_and_more.py +35 -0
  114. wbportfolio/models/__init__.py +2 -0
  115. wbportfolio/models/adjustments.py +1 -1
  116. wbportfolio/models/asset.py +28 -170
  117. wbportfolio/models/builder.py +323 -0
  118. wbportfolio/models/custodians.py +3 -3
  119. wbportfolio/models/exceptions.py +1 -1
  120. wbportfolio/models/graphs/portfolio.py +1 -1
  121. wbportfolio/models/graphs/utils.py +11 -11
  122. wbportfolio/models/mixins/instruments.py +7 -0
  123. wbportfolio/models/mixins/liquidity_stress_test.py +4 -4
  124. wbportfolio/models/orders/__init__.py +2 -0
  125. wbportfolio/models/orders/order_proposals.py +1414 -0
  126. wbportfolio/models/orders/orders.py +410 -0
  127. wbportfolio/models/portfolio.py +311 -289
  128. wbportfolio/models/portfolio_relationship.py +6 -0
  129. wbportfolio/models/products.py +12 -0
  130. wbportfolio/models/{transactions/rebalancing.py → rebalancing.py} +40 -27
  131. wbportfolio/models/roles.py +4 -10
  132. wbportfolio/models/transactions/__init__.py +0 -4
  133. wbportfolio/models/transactions/claim.py +7 -6
  134. wbportfolio/models/transactions/dividends.py +42 -5
  135. wbportfolio/models/transactions/fees.py +55 -22
  136. wbportfolio/models/transactions/trades.py +121 -442
  137. wbportfolio/models/transactions/transactions.py +78 -158
  138. wbportfolio/models/utils.py +100 -1
  139. wbportfolio/order_routing/__init__.py +35 -0
  140. wbportfolio/order_routing/adapters/__init__.py +65 -0
  141. wbportfolio/order_routing/adapters/ubs.py +195 -0
  142. wbportfolio/order_routing/router.py +33 -0
  143. wbportfolio/order_routing/tests/__init__.py +0 -0
  144. wbportfolio/order_routing/tests/test_router.py +110 -0
  145. wbportfolio/permissions.py +7 -0
  146. wbportfolio/pms/analytics/portfolio.py +17 -9
  147. wbportfolio/pms/analytics/utils.py +9 -0
  148. wbportfolio/pms/trading/__init__.py +0 -1
  149. wbportfolio/pms/trading/optimizer.py +61 -0
  150. wbportfolio/pms/typing.py +198 -63
  151. wbportfolio/rebalancing/base.py +12 -1
  152. wbportfolio/rebalancing/decorators.py +1 -1
  153. wbportfolio/rebalancing/models/composite.py +4 -8
  154. wbportfolio/rebalancing/models/equally_weighted.py +13 -11
  155. wbportfolio/rebalancing/models/market_capitalization_weighted.py +21 -14
  156. wbportfolio/rebalancing/models/model_portfolio.py +14 -18
  157. wbportfolio/risk_management/backends/__init__.py +1 -0
  158. wbportfolio/risk_management/backends/controversy_portfolio.py +2 -2
  159. wbportfolio/risk_management/backends/esg_aggregation_portfolio.py +64 -0
  160. wbportfolio/risk_management/backends/exposure_portfolio.py +4 -4
  161. wbportfolio/risk_management/backends/instrument_list_portfolio.py +3 -3
  162. wbportfolio/risk_management/tests/test_esg_aggregation_portfolio.py +49 -0
  163. wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
  164. wbportfolio/risk_management/tests/test_stop_loss_instrument.py +2 -2
  165. wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -1
  166. wbportfolio/serializers/__init__.py +1 -0
  167. wbportfolio/serializers/orders/__init__.py +2 -0
  168. wbportfolio/serializers/orders/order_proposals.py +115 -0
  169. wbportfolio/serializers/orders/orders.py +283 -0
  170. wbportfolio/serializers/portfolios.py +7 -7
  171. wbportfolio/serializers/positions.py +2 -2
  172. wbportfolio/serializers/rebalancing.py +1 -1
  173. wbportfolio/serializers/signals.py +9 -12
  174. wbportfolio/serializers/transactions/__init__.py +1 -10
  175. wbportfolio/serializers/transactions/claim.py +2 -2
  176. wbportfolio/serializers/transactions/dividends.py +37 -9
  177. wbportfolio/serializers/transactions/fees.py +39 -10
  178. wbportfolio/serializers/transactions/trades.py +55 -157
  179. wbportfolio/tasks.py +43 -5
  180. wbportfolio/tests/analysis/__init__.py +0 -0
  181. wbportfolio/tests/analysis/test_claims.py +85 -0
  182. wbportfolio/tests/conftest.py +12 -12
  183. wbportfolio/tests/models/orders/__init__.py +0 -0
  184. wbportfolio/tests/models/orders/test_order_proposals.py +1046 -0
  185. wbportfolio/tests/models/test_assets.py +7 -3
  186. wbportfolio/tests/models/test_imports.py +9 -13
  187. wbportfolio/tests/models/test_portfolios.py +102 -95
  188. wbportfolio/tests/models/test_products.py +11 -0
  189. wbportfolio/tests/models/test_splits.py +1 -6
  190. wbportfolio/tests/models/test_utils.py +140 -0
  191. wbportfolio/tests/models/transactions/test_fees.py +7 -13
  192. wbportfolio/tests/models/transactions/test_rebalancing.py +5 -5
  193. wbportfolio/tests/models/transactions/test_trades.py +0 -20
  194. wbportfolio/tests/pms/test_analytics.py +22 -3
  195. wbportfolio/tests/rebalancing/test_models.py +51 -57
  196. wbportfolio/tests/signals.py +10 -20
  197. wbportfolio/tests/tests.py +3 -1
  198. wbportfolio/tests/viewsets/test_products.py +1 -0
  199. wbportfolio/urls.py +10 -13
  200. wbportfolio/viewsets/__init__.py +9 -4
  201. wbportfolio/viewsets/assets.py +3 -204
  202. wbportfolio/viewsets/charts/__init__.py +6 -1
  203. wbportfolio/viewsets/charts/assets.py +344 -154
  204. wbportfolio/viewsets/configs/buttons/__init__.py +2 -2
  205. wbportfolio/viewsets/configs/buttons/assets.py +1 -1
  206. wbportfolio/viewsets/configs/buttons/mixins.py +4 -4
  207. wbportfolio/viewsets/configs/buttons/portfolios.py +45 -1
  208. wbportfolio/viewsets/configs/buttons/products.py +32 -2
  209. wbportfolio/viewsets/configs/display/__init__.py +2 -5
  210. wbportfolio/viewsets/configs/display/assets.py +6 -19
  211. wbportfolio/viewsets/configs/display/fees.py +3 -3
  212. wbportfolio/viewsets/configs/display/portfolios.py +5 -5
  213. wbportfolio/viewsets/configs/display/products.py +1 -1
  214. wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
  215. wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
  216. wbportfolio/viewsets/configs/display/trades.py +1 -189
  217. wbportfolio/viewsets/configs/endpoints/__init__.py +3 -7
  218. wbportfolio/viewsets/configs/endpoints/fees.py +2 -2
  219. wbportfolio/viewsets/configs/endpoints/trades.py +0 -41
  220. wbportfolio/viewsets/configs/menu/__init__.py +1 -1
  221. wbportfolio/viewsets/configs/menu/orders.py +11 -0
  222. wbportfolio/viewsets/configs/titles/__init__.py +2 -3
  223. wbportfolio/viewsets/configs/titles/fees.py +4 -8
  224. wbportfolio/viewsets/esg.py +3 -5
  225. wbportfolio/viewsets/mixins.py +5 -1
  226. wbportfolio/viewsets/orders/__init__.py +6 -0
  227. wbportfolio/viewsets/orders/configs/__init__.py +4 -0
  228. wbportfolio/viewsets/orders/configs/buttons/__init__.py +2 -0
  229. wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +188 -0
  230. wbportfolio/viewsets/orders/configs/buttons/orders.py +113 -0
  231. wbportfolio/viewsets/orders/configs/displays/__init__.py +2 -0
  232. wbportfolio/viewsets/orders/configs/displays/order_proposals.py +157 -0
  233. wbportfolio/viewsets/orders/configs/displays/orders.py +232 -0
  234. wbportfolio/viewsets/orders/configs/endpoints/__init__.py +2 -0
  235. wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +21 -0
  236. wbportfolio/viewsets/orders/configs/endpoints/orders.py +28 -0
  237. wbportfolio/viewsets/orders/configs/titles/__init__.py +0 -0
  238. wbportfolio/viewsets/orders/configs/titles/orders.py +0 -0
  239. wbportfolio/viewsets/orders/order_proposals.py +252 -0
  240. wbportfolio/viewsets/orders/orders.py +277 -0
  241. wbportfolio/viewsets/portfolios.py +36 -12
  242. wbportfolio/viewsets/positions.py +3 -2
  243. wbportfolio/viewsets/products.py +6 -6
  244. wbportfolio/viewsets/{transactions/rebalancing.py → rebalancing.py} +2 -2
  245. wbportfolio/viewsets/transactions/__init__.py +3 -14
  246. wbportfolio/viewsets/transactions/fees.py +22 -22
  247. wbportfolio/viewsets/transactions/trades.py +1 -180
  248. {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/METADATA +3 -1
  249. {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/RECORD +252 -203
  250. {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/WHEEL +1 -1
  251. wbportfolio/admin/transactions/transactions.py +0 -38
  252. wbportfolio/factories/transactions.py +0 -22
  253. wbportfolio/fdm/tasks.py +0 -13
  254. wbportfolio/filters/transactions/transactions.py +0 -99
  255. wbportfolio/models/transactions/expiry.py +0 -7
  256. wbportfolio/models/transactions/trade_proposals.py +0 -704
  257. wbportfolio/pms/trading/handler.py +0 -161
  258. wbportfolio/serializers/transactions/expiry.py +0 -18
  259. wbportfolio/serializers/transactions/trade_proposals.py +0 -76
  260. wbportfolio/serializers/transactions/transactions.py +0 -85
  261. wbportfolio/tests/models/transactions/test_trade_proposals.py +0 -410
  262. wbportfolio/viewsets/configs/buttons/trade_proposals.py +0 -66
  263. wbportfolio/viewsets/configs/display/trade_proposals.py +0 -100
  264. wbportfolio/viewsets/configs/display/transactions.py +0 -55
  265. wbportfolio/viewsets/configs/endpoints/trade_proposals.py +0 -18
  266. wbportfolio/viewsets/configs/endpoints/transactions.py +0 -14
  267. wbportfolio/viewsets/configs/menu/transactions.py +0 -9
  268. wbportfolio/viewsets/configs/titles/transactions.py +0 -9
  269. wbportfolio/viewsets/signals.py +0 -43
  270. wbportfolio/viewsets/transactions/trade_proposals.py +0 -139
  271. wbportfolio/viewsets/transactions/transactions.py +0 -122
  272. /wbportfolio/{fdm → api_clients}/__init__.py +0 -0
  273. {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,49 @@
1
+ # Generated by Django 5.0.12 on 2025-11-11 10:16
2
+
3
+ import django_fsm
4
+ import django.db.models.deletion
5
+ from decimal import Decimal
6
+ from django.db import migrations, models
7
+ import django_fsm
8
+ from django.db import migrations
9
+
10
+ def migrate_status(apps, schema_editor):
11
+ OrderProposal = apps.get_model("wbportfolio", "OrderProposal")
12
+ OrderProposal.objects.filter(status="APPLIED").update(status="CONFIRMED")
13
+
14
+
15
+ class Migration(migrations.Migration):
16
+
17
+ dependencies = [
18
+ ('wbportfolio', '0091_remove_order_execution_confirmed_and_more'),
19
+ ]
20
+
21
+ operations = [
22
+ migrations.AddField(
23
+ model_name='order',
24
+ name='quantization_error',
25
+ field=models.DecimalField(decimal_places=8, default=Decimal('0'), max_digits=9, verbose_name='Quantization Error'),
26
+ ),
27
+ migrations.AlterField(
28
+ model_name='orderproposal',
29
+ name='status',
30
+ field=django_fsm.FSMField(choices=[('DRAFT', 'Draft'), ('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('DENIED', 'Denied'), ('EXECUTION', 'Execution'), ('APPLIED', 'Applied'), ('FAILED', 'Failed')], default='DRAFT', max_length=50, verbose_name='Status'),
31
+ ),
32
+ migrations.AlterField(
33
+ model_name='orderproposal',
34
+ name='status',
35
+ field=django_fsm.FSMField(
36
+ choices=[('DRAFT', 'Draft'), ('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('DENIED', 'Denied'),
37
+ ('EXECUTION', 'Execution'), ('CONFIRMED', 'Confirmed'), ('FAILED', 'Failed')], default='DRAFT',
38
+ max_length=50, verbose_name='Status'),
39
+ ),
40
+ migrations.AddField(
41
+ model_name='order',
42
+ name='execution_trade',
43
+ field=models.OneToOneField(blank=True, help_text='The executed Trade', null=True,
44
+ on_delete=django.db.models.deletion.SET_NULL, related_name='order',
45
+ to='wbportfolio.trade'),
46
+ ),
47
+ migrations.RunPython(migrate_status),
48
+
49
+ ]
@@ -0,0 +1,35 @@
1
+ # Generated by Django 5.0.12 on 2025-11-28 13:11
2
+
3
+ from django.db import migrations, models
4
+
5
+ def handle_data(apps, schema_editor):
6
+ PortfolioPortfolioThroughModel = apps.get_model('wbportfolio', 'PortfolioPortfolioThroughModel')
7
+ PortfolioPortfolioThroughModel.objects.filter(type="PRIMARY").update(type="LOOK_THROUGH")
8
+ from wbportfolio.models.portfolio import Portfolio
9
+ for portfolio in Portfolio.objects.all():
10
+ if portfolio.assets.exists():
11
+ val_date = portfolio.assets.latest("date").date
12
+ for parent_ptf, _ in portfolio.get_parent_portfolios(val_date):
13
+ PortfolioPortfolioThroughModel.objects.get_or_create(portfolio_id=portfolio.id, dependency_portfolio_id=parent_ptf.id, defaults={"type": "HIERARCHICAL"})
14
+ class Migration(migrations.Migration):
15
+
16
+ dependencies = [
17
+ ('wbportfolio', '0092_order_quantization_error_alter_orderproposal_status'),
18
+ ]
19
+
20
+ operations = [
21
+ migrations.RemoveConstraint(
22
+ model_name='portfolioportfoliothroughmodel',
23
+ name='unique_primary',
24
+ ),
25
+ migrations.AlterField(
26
+ model_name='portfolioportfoliothroughmodel',
27
+ name='type',
28
+ field=models.CharField(choices=[('LOOK_THROUGH', 'Look-through'), ('MODEL', 'Model'), ('CUSTODIAN', 'Custodian'), ('HIERARCHICAL', 'Hierarchical')], default='LOOK_THROUGH', verbose_name='Type'),
29
+ ),
30
+ migrations.AddConstraint(
31
+ model_name='portfolioportfoliothroughmodel',
32
+ constraint=models.UniqueConstraint(condition=models.Q(('type', 'LOOK_THROUGH')), fields=('portfolio', 'type'), name='unique_lookthrough'),
33
+ ),
34
+ migrations.RunPython(handle_data)
35
+ ]
@@ -17,5 +17,7 @@ from .portfolio_cash_flow import DailyPortfolioCashFlow
17
17
  from .portfolio_swing_pricings import PortfolioSwingPricing
18
18
  from .registers import Register
19
19
  from .transactions import *
20
+ from .orders import *
20
21
  from .reconciliations import AccountReconciliation, AccountReconciliationLine
21
22
  from .signals import *
23
+ from .rebalancing import RebalancingModel, Rebalancer
@@ -212,7 +212,7 @@ def post_adjustment_on_prices(adjustment_id, automatically_confirm_approve_adjus
212
212
  adjustment.apply_adjustment_on_assets()
213
213
  adjustment.status = Adjustment.Status.APPLIED
214
214
  else:
215
- for user in User.objects.filter(profile__in=PortfolioRole.portfolio_managers()):
215
+ for user in User.objects.filter(profile__in=PortfolioRole.portfolio_managers(), is_active=True):
216
216
  send_notification(
217
217
  code="wbportfolio.adjustment.add",
218
218
  title="A new adjustment was imported",
@@ -1,10 +1,11 @@
1
- from collections import defaultdict
1
+ import logging
2
2
  from contextlib import suppress
3
3
  from datetime import date
4
4
  from decimal import Decimal, InvalidOperation
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from django.contrib import admin
8
+ from django.core.exceptions import ObjectDoesNotExist
8
9
  from django.db import models
9
10
  from django.db.models import (
10
11
  Case,
@@ -30,7 +31,6 @@ from pandas._libs.tslibs.offsets import BDay
30
31
  from wbcore.contrib.currency.models import Currency, CurrencyFXRates
31
32
  from wbcore.contrib.io.mixins import ImportMixin
32
33
  from wbcore.signals import pre_merge
33
- from wbcore.utils.enum import ChoiceEnum
34
34
  from wbfdm.models import Classification, ClassificationGroup, Instrument
35
35
  from wbfdm.models.instruments.instrument_prices import InstrumentPrice
36
36
  from wbfdm.signals import add_instrument_to_investable_universe
@@ -43,6 +43,8 @@ from wbportfolio.models.portfolio_relationship import (
43
43
  from wbportfolio.models.roles import PortfolioRole
44
44
  from wbportfolio.pms.typing import Position as PositionDTO
45
45
 
46
+ logger = logging.getLogger("pms")
47
+
46
48
  MARKETCAP_S = 2_000_000_000
47
49
  MARKETCAP_M = 10_000_000_000
48
50
  MARKETCAP_L = 50_000_000_000
@@ -55,145 +57,7 @@ HOUR = MINUTE * 60
55
57
  DAY = HOUR * 24
56
58
 
57
59
  if TYPE_CHECKING:
58
- from wbportfolio.models.portfolio import Portfolio
59
-
60
-
61
- class AssetPositionIterator:
62
- """
63
- Efficiently converts position data into AssetPosition models with batch operations
64
- and proper dependency management.
65
-
66
- Features:
67
- - Bulk database fetching for performance
68
- - Thread-safe operations
69
- - Clear type hints
70
- - Memory-efficient storage
71
- """
72
-
73
- positions: dict[tuple[date, int, int | None], "AssetPosition"]
74
-
75
- _prices: dict[date, dict[int, float]]
76
- _weights: dict[date, dict[int, float]]
77
- _fx_rates: dict[date, dict[Currency, CurrencyFXRates]]
78
- _instruments: dict[int, Instrument]
79
-
80
- def __init__(
81
- self,
82
- portfolio: "Portfolio",
83
- prices: dict[date, dict[int, float]] | None = None,
84
- infer_underlying_quote_price: bool = False,
85
- ):
86
- self.portfolio = portfolio
87
- self.infer_underlying_quote_price = infer_underlying_quote_price
88
- # Initialize data stores with type hints
89
- self._instruments = {}
90
- self._fx_rates = defaultdict(dict)
91
- self._weights = defaultdict(dict)
92
- self._prices = prices or defaultdict(dict)
93
- self.positions = dict()
94
-
95
- def _get_instrument(self, instrument_id: int) -> Instrument:
96
- try:
97
- return self._instruments[instrument_id]
98
- except KeyError:
99
- instrument = Instrument.objects.get(id=instrument_id)
100
- self._instruments[instrument_id] = instrument
101
- return instrument
102
-
103
- def _get_fx_rate(self, val_date: date, currency: Currency) -> CurrencyFXRates | None:
104
- try:
105
- return self._fx_rates[val_date][currency]
106
- except KeyError:
107
- with suppress(CurrencyFXRates.DoesNotExist):
108
- fx_rate = CurrencyFXRates.objects.get(
109
- currency=currency, date=val_date
110
- ) # we create a fx rate anyway to not fail the position. The fx rate expect to be there later on
111
- self._fx_rates[val_date][currency] = fx_rate
112
- return fx_rate
113
-
114
- def _get_price(self, val_date: date, instrument: Instrument) -> float | None:
115
- try:
116
- return self._prices[val_date][instrument.id]
117
- except KeyError:
118
- return None
119
-
120
- def _dict_to_model(self, val_date: date, instrument_id: int, weighting: float) -> "AssetPosition":
121
- underlying_quote = self._get_instrument(instrument_id)
122
- currency_fx_rate_portfolio_to_usd = self._get_fx_rate(val_date, self.portfolio.currency)
123
- currency_fx_rate_instrument_to_usd = self._get_fx_rate(val_date, underlying_quote.currency)
124
- price = self._get_price(val_date, underlying_quote)
125
- position = AssetPosition(
126
- underlying_quote=underlying_quote,
127
- weighting=weighting,
128
- date=val_date,
129
- asset_valuation_date=val_date,
130
- is_estimated=True,
131
- portfolio=self.portfolio,
132
- currency=underlying_quote.currency,
133
- initial_price=price,
134
- currency_fx_rate_portfolio_to_usd=currency_fx_rate_portfolio_to_usd,
135
- currency_fx_rate_instrument_to_usd=currency_fx_rate_instrument_to_usd,
136
- initial_currency_fx_rate=None,
137
- underlying_quote_price=None,
138
- underlying_instrument=None,
139
- )
140
- position.pre_save(
141
- infer_underlying_quote_price=self.infer_underlying_quote_price
142
- ) # inferring underlying quote price is potentially very slow for big dataset of positions, it's not very needed for model portfolio so we disable it
143
- return position
144
-
145
- def add(
146
- self,
147
- positions: list["AssetPosition"] | tuple[date, dict[int, float]],
148
- ):
149
- """
150
- Add multiple positions efficiently with batch processing
151
-
152
- Args:
153
- positions: Iterable of AssetPosition instances or dictionary of weight {instrument_id: weight} that needs to be converted into AssetPosition
154
- """
155
- if isinstance(positions, tuple):
156
- val_date = positions[0]
157
- positions = [(val_date, i, w) for i, w in positions[1].items()] # unflatten data to make it iterable
158
- for position in positions:
159
- if not isinstance(position, AssetPosition):
160
- position = self._dict_to_model(*position)
161
-
162
- # Generate unique composite key
163
- key = (
164
- position.date,
165
- position.underlying_quote.id,
166
- position.portfolio_created.id if position.portfolio_created else None,
167
- )
168
- # Merge duplicate positions
169
- if existing_position := self.positions.get(key):
170
- position.weighting += existing_position.weighting
171
- position.initial_shares += existing_position.initial_shares
172
- # ensure the position portfolio is the iterator portfolio (could be different when computing look-through for instance)
173
- position.portfolio = self.portfolio
174
- if position.initial_price is not None and position.initial_currency_fx_rate is not None:
175
- self.positions[key] = position
176
- self._weights[position.date][position.underlying_quote.id] = float(position.weighting)
177
-
178
- return self
179
-
180
- def get_dates(self) -> list[date]:
181
- """Get sorted list of unique dates"""
182
- return list(self._weights.keys())
183
-
184
- def get_weights(self) -> dict[date, dict[int, float]]:
185
- """Get weight structure with instrument IDs as keys"""
186
- return dict(self._weights)
187
-
188
- def __iter__(self):
189
- # return an iterable excluding the position with a null weight if the portfolio is manageable (otherwise, we assume the 0-weight position is valid)
190
- yield from filter(lambda a: not a.portfolio.is_manageable or a.weighting, self.positions.values())
191
-
192
- def __getitem__(self, item: tuple[date, Instrument]) -> float:
193
- return self._weights[item[0]][item[1].id]
194
-
195
- def __bool__(self) -> bool:
196
- return len(self.positions.keys()) > 0
60
+ pass
197
61
 
198
62
 
199
63
  class AssetPositionDefaultQueryset(QuerySet):
@@ -359,25 +223,12 @@ class AnalyticalAssetPositionManager(DefaultAssetPositionManager):
359
223
  )
360
224
 
361
225
 
362
- class AssetPositionGroupBy(ChoiceEnum):
363
- INDUSTRY = "Industry"
364
- COUNTRY = "Country"
365
- CURRENCY = "Currency"
366
- CASH = "Cash"
367
- MARKET_CAPITALIZATION = "Market Cap"
368
- LIQUIDITY = "Liquidity"
369
-
370
- @classmethod
371
- def get_class_method_group_by(cls, name: str):
372
- _map = {
373
- "INDUSTRY": "industry",
374
- "COUNTRY": AssetPosition.country_group_by,
375
- "CURRENCY": AssetPosition.currency_group_by,
376
- "CASH": AssetPosition.cash_group_by,
377
- "MARKET_CAPITALIZATION": AssetPosition.marketcap_group_by,
378
- "LIQUIDITY": AssetPosition.liquidity_group_by,
379
- }
380
- return _map[name]
226
+ class AssetPositionGroupBy(models.TextChoices):
227
+ INDUSTRY = "classification", "Industry"
228
+ INSTRUMENT_TYPE = "instrument_type", "Type"
229
+ COUNTRY = "country", "Country"
230
+ CURRENCY = "currency", "Currency"
231
+ CASH = "is_cash", "Cash"
381
232
 
382
233
 
383
234
  class AssetPosition(ImportMixin, models.Model):
@@ -464,8 +315,8 @@ class AssetPosition(ImportMixin, models.Model):
464
315
  # )
465
316
 
466
317
  weighting = models.DecimalField(
467
- decimal_places=6,
468
- max_digits=7,
318
+ decimal_places=8,
319
+ max_digits=9,
469
320
  default=Decimal(0),
470
321
  verbose_name="Weight",
471
322
  help_text="The Weight of the Asset on the price date of the Asset.",
@@ -558,7 +409,7 @@ class AssetPosition(ImportMixin, models.Model):
558
409
  analytical_objects = AnalyticalAssetPositionManager()
559
410
  unannotated_objects = models.Manager()
560
411
 
561
- def pre_save(
412
+ def pre_save( # noqa: C901
562
413
  self, create_underlying_quote_price_if_missing: bool = False, infer_underlying_quote_price: bool = True
563
414
  ):
564
415
  if not self.asset_valuation_date:
@@ -579,12 +430,12 @@ class AssetPosition(ImportMixin, models.Model):
579
430
  ):
580
431
  try:
581
432
  self.underlying_quote = self.underlying_instrument.children.get(is_primary=True)
582
- except:
433
+ except ObjectDoesNotExist:
583
434
  self.underlying_quote = self.underlying_instrument
584
435
 
585
436
  if not getattr(self, "currency", None):
586
437
  self.currency = self.underlying_quote.currency
587
- if not self.underlying_quote_price and infer_underlying_quote_price:
438
+ if not self.underlying_quote_price and (infer_underlying_quote_price or not self.initial_price):
588
439
  try:
589
440
  # 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
590
441
  # Instead, we decided to opt for a post_save based system that will assign the missing position price when a price is created
@@ -597,7 +448,10 @@ class AssetPosition(ImportMixin, models.Model):
597
448
  net_value = self.initial_price
598
449
  # in case the position currency and the linked underlying_quote currency don't correspond, we convert the rate accordingly
599
450
  if self.currency != self.underlying_quote.currency:
600
- net_value *= self.currency.convert(self.asset_valuation_date, self.underlying_quote.currency)
451
+ with suppress(CurrencyFXRates.DoesNotExist):
452
+ net_value *= self.currency.convert(
453
+ self.asset_valuation_date, self.underlying_quote.currency
454
+ )
601
455
  self.underlying_quote_price = InstrumentPrice.objects.create(
602
456
  calculated=False,
603
457
  instrument=self.underlying_quote,
@@ -652,6 +506,8 @@ class AssetPosition(ImportMixin, models.Model):
652
506
  portfolio_created=self.portfolio_created,
653
507
  )
654
508
  self.initial_shares = previous_pos.initial_shares
509
+ if self.underlying_quote:
510
+ self.exchange = self.underlying_quote.exchange
655
511
 
656
512
  def save(self, *args, create_underlying_quote_price_if_missing: bool = False, **kwargs):
657
513
  self.pre_save(create_underlying_quote_price_if_missing=create_underlying_quote_price_if_missing)
@@ -691,22 +547,22 @@ class AssetPosition(ImportMixin, models.Model):
691
547
  def get_portfolio_total_asset_value(self) -> Decimal:
692
548
  return self.portfolio.get_total_asset_value(self.date)
693
549
 
694
- def _build_dto(self, new_weight: Decimal = None) -> PositionDTO:
550
+ def _build_dto(self, **kwargs) -> PositionDTO:
695
551
  """
696
552
  Data Transfer Object
697
553
  Returns:
698
554
  DTO position object
699
555
  """
700
- return PositionDTO(
556
+ parameters = dict(
701
557
  underlying_instrument=self.underlying_quote.id,
702
- weighting=self.weighting if new_weight is None else new_weight,
558
+ weighting=self.weighting,
703
559
  shares=self._shares,
704
560
  date=self.date,
705
561
  asset_valuation_date=self.asset_valuation_date,
706
562
  instrument_type=self.underlying_quote.security_instrument_type.id,
707
563
  currency=self.underlying_quote.currency.id,
708
564
  country=self.underlying_quote.country.id if self.underlying_quote.country else None,
709
- is_cash=self.underlying_quote.is_cash,
565
+ is_cash=self.underlying_quote.is_cash or self.underlying_quote.is_cash_equivalent,
710
566
  primary_classification=(
711
567
  self.underlying_quote.primary_classification.id
712
568
  if self.underlying_quote.primary_classification
@@ -725,6 +581,8 @@ class AssetPosition(ImportMixin, models.Model):
725
581
  currency_fx_rate=self._currency_fx_rate,
726
582
  portfolio_created=self.portfolio_created.id if self.portfolio_created else None,
727
583
  )
584
+ parameters.update(kwargs)
585
+ return PositionDTO(**parameters)
728
586
 
729
587
  @cached_property
730
588
  @admin.display(description="Adjusting Factor (adjustment)")