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,30 +1,32 @@
1
+ import logging
1
2
  from contextlib import suppress
2
- from datetime import timedelta
3
+ from datetime import date, timedelta
4
+ from decimal import Decimal
3
5
  from typing import TypeVar
4
6
 
5
- import pandas as pd
6
7
  from celery import shared_task
7
8
  from django.core.exceptions import ValidationError
8
9
  from django.db import models
9
- from django.db.models.signals import post_save
10
- from django.dispatch import receiver
11
- from django.utils import timezone
12
10
  from django.utils.functional import cached_property
13
11
  from django_fsm import FSMField, transition
14
- from pandas.tseries.offsets import BDay
12
+ from pandas._libs.tslibs.offsets import BDay
15
13
  from wbcompliance.models.risk_management.mixins import RiskCheckMixin
16
14
  from wbcore.contrib.icons import WBIcon
17
15
  from wbcore.enums import RequestType
18
16
  from wbcore.metadata.configs.buttons import ActionButton
19
17
  from wbcore.models import WBModel
20
18
  from wbfdm.models.instruments.instruments import Instrument
19
+
21
20
  from wbportfolio.models.roles import PortfolioRole
22
21
  from wbportfolio.pms.trading import TradingService
23
22
  from wbportfolio.pms.typing import Portfolio as PortfolioDTO
24
23
  from wbportfolio.pms.typing import TradeBatch as TradeBatchDTO
25
24
 
25
+ from .. import AssetPosition
26
26
  from .trades import Trade
27
27
 
28
+ logger = logging.getLogger("pms")
29
+
28
30
  SelfTradeProposal = TypeVar("SelfTradeProposal", bound="TradeProposal")
29
31
 
30
32
 
@@ -39,13 +41,14 @@ class TradeProposal(RiskCheckMixin, WBModel):
39
41
 
40
42
  comment = models.TextField(default="", verbose_name="Trade Comment", blank=True)
41
43
  status = FSMField(default=Status.DRAFT, choices=Status.choices, verbose_name="Status")
42
- model_portfolio = models.ForeignKey(
43
- "wbportfolio.Portfolio",
44
+ rebalancing_model = models.ForeignKey(
45
+ "wbportfolio.RebalancingModel",
46
+ on_delete=models.SET_NULL,
44
47
  blank=True,
45
48
  null=True,
46
- related_name="model_trade_proposals",
47
- on_delete=models.PROTECT,
48
- verbose_name="Model Portfolio",
49
+ related_name="trade_proposals",
50
+ verbose_name="Rebalancing Model",
51
+ help_text="Rebalancing Model that generates the target portfolio",
49
52
  )
50
53
  portfolio = models.ForeignKey(
51
54
  "wbportfolio.Portfolio", related_name="trade_proposals", on_delete=models.PROTECT, verbose_name="Portfolio"
@@ -59,6 +62,23 @@ class TradeProposal(RiskCheckMixin, WBModel):
59
62
  verbose_name="Owner",
60
63
  )
61
64
 
65
+ class Meta:
66
+ verbose_name = "Trade Proposal"
67
+ verbose_name_plural = "Trade Proposals"
68
+ constraints = [
69
+ models.UniqueConstraint(
70
+ fields=["portfolio", "trade_date"],
71
+ name="unique_trade_proposal",
72
+ ),
73
+ ]
74
+
75
+ def save(self, *args, **kwargs):
76
+ if not self.trade_date and self.portfolio.assets.exists():
77
+ self.trade_date = (self.portfolio.assets.latest("date").date + BDay(1)).date()
78
+ if not self.rebalancing_model and (rebalancer := getattr(self.portfolio, "automatic_rebalancer", None)):
79
+ self.rebalancing_model = rebalancer.rebalancing_model
80
+ super().save(*args, **kwargs)
81
+
62
82
  def _get_checked_object_field_name(self) -> str:
63
83
  """
64
84
  Mandatory function from the Riskcheck mixin that returns the field (aka portfolio), representing the object to check the rules against.
@@ -76,6 +96,13 @@ class TradeProposal(RiskCheckMixin, WBModel):
76
96
  trades_batch=self._build_dto(),
77
97
  )
78
98
 
99
+ @cached_property
100
+ def last_effective_date(self) -> date:
101
+ try:
102
+ return self.portfolio.assets.filter(date__lt=self.trade_date).latest("date").date
103
+ except AssetPosition.DoesNotExist:
104
+ return (self.trade_date - BDay(1)).date()
105
+
79
106
  @property
80
107
  def previous_trade_proposal(self) -> SelfTradeProposal | None:
81
108
  future_proposals = TradeProposal.objects.filter(portfolio=self.portfolio).filter(
@@ -94,8 +121,8 @@ class TradeProposal(RiskCheckMixin, WBModel):
94
121
  return future_proposals.earliest("trade_date")
95
122
  return None
96
123
 
97
- @cached_property
98
- def base_assets(self):
124
+ @property
125
+ def base_assets(self) -> dict[int, Decimal]:
99
126
  """
100
127
  Return a dictionary representation (instrument_id: target weight) of this trade proposal
101
128
  Returns:
@@ -104,17 +131,15 @@ class TradeProposal(RiskCheckMixin, WBModel):
104
131
  """
105
132
  return {
106
133
  v["underlying_instrument"]: v["target_weight"]
107
- for v in self.trades.filter(status=Trade.Status.EXECUTED).values("underlying_instrument", "target_weight")
134
+ for v in self.trades.all()
135
+ .annotate_base_info()
136
+ .filter(status=Trade.Status.EXECUTED)
137
+ .values("underlying_instrument", "target_weight")
108
138
  }
109
139
 
110
140
  def __str__(self) -> str:
111
141
  return f"{self.portfolio.name}: {self.trade_date} ({self.status})"
112
142
 
113
- def save(self, *args, **kwargs):
114
- if not self.model_portfolio:
115
- self.model_portfolio = self.portfolio
116
- super().save(*args, **kwargs)
117
-
118
143
  def _build_dto(self) -> TradeBatchDTO:
119
144
  """
120
145
  Data Transfer Object
@@ -144,17 +169,10 @@ class TradeProposal(RiskCheckMixin, WBModel):
144
169
  trade_date=trade_date,
145
170
  comment=kwargs.get("comment", self.comment),
146
171
  status=TradeProposal.Status.DRAFT,
147
- model_portfolio=self.model_portfolio,
172
+ rebalancing_model=self.rebalancing_model,
148
173
  portfolio=self.portfolio,
149
174
  creator=self.creator,
150
175
  )
151
-
152
- # For all existing trades, copy them to the new trade proposal
153
- for trade in self.trades.all():
154
- trade.pk = None
155
- trade.trade_proposal = trade_proposal_clone
156
- trade.transaction_date = trade_proposal_clone.trade_date
157
- trade.save()
158
176
  return trade_proposal_clone
159
177
 
160
178
  def normalize_trades(self):
@@ -167,111 +185,96 @@ class TradeProposal(RiskCheckMixin, WBModel):
167
185
  leftovers_trades = self.trades.all()
168
186
  for _, trade in service.trades_batch.trades_map.items():
169
187
  with suppress(Trade.DoesNotExist):
170
- self.trades.update_or_create(
171
- id=trade.id,
172
- defaults={
173
- "weighting": trade.delta_weight,
174
- "shares": trade.target_shares,
175
- },
176
- )
188
+ trade = Trade.objects.get(id=trade.id)
189
+ trade.weighting = trade.delta_weight
190
+ trade.shares = self.estimate_shares(trade)
191
+ trade.save()
177
192
  leftovers_trades = leftovers_trades.exclude(id=trade.id)
178
193
  leftovers_trades.delete()
179
194
 
180
- def reset_trades(self):
195
+ def _get_target_portfolio(self, **kwargs) -> PortfolioDTO:
196
+ if self.rebalancing_model:
197
+ return self.rebalancing_model.get_target_portfolio(
198
+ self.portfolio, self.trade_date, self.last_effective_date, **kwargs
199
+ )
200
+ # Return the current portfolio by default
201
+ return self.portfolio._build_dto(self.last_effective_date)
202
+
203
+ def reset_trades(self, target_portfolio: PortfolioDTO | None = None):
181
204
  """
182
205
  Will delete all existing trades and recreate them from the method `create_or_update_trades`
183
206
  """
207
+ if self.status != TradeProposal.Status.DRAFT:
208
+ raise ValueError("Cannot reset non-draft trade proposal. Revert this trade proposal first.")
184
209
  # delete all existing trades
185
210
  self.trades.all().delete()
186
- # recreate them from scratch (if the portfolio has positions)
187
- self.create_or_update_trades()
211
+ last_effective_date = self.last_effective_date
212
+ # Get effective and target portfolio
213
+ effective_portfolio = self.portfolio._build_dto(last_effective_date)
214
+ if not target_portfolio:
215
+ target_portfolio = self._get_target_portfolio()
216
+ # if not effective_portfolio:
217
+ # effective_portfolio = target_portfolio
218
+ service = TradingService(
219
+ self.trade_date,
220
+ effective_portfolio=effective_portfolio,
221
+ target_portfolio=target_portfolio,
222
+ )
223
+ service.normalize()
224
+ service.is_valid()
188
225
 
189
- def apply_trades(self):
190
- # We validate trade which will create or update the initial asset positions
191
- self.trades.exclude(status=Trade.Status.SUBMIT).update(status=Trade.Status.SUBMIT)
192
- for trade in self.trades.all():
193
- trade.execute()
194
- trade.save()
195
- # We propagate the new portfolio composition until the next trade proposal or today if it doesn't exist yet
196
- to_date = self.next_trade_proposal.trade_date if self.next_trade_proposal else timezone.now().date()
197
-
198
- for from_date in pd.date_range(self.trade_date, to_date - timedelta(days=1), freq="B"):
199
- to_date = (from_date + BDay(1)).date()
200
- self.portfolio.propagate_or_update_assets(
201
- from_date.date(),
202
- to_date,
203
- forward_price=False,
204
- base_assets=self.base_assets,
205
- delete_existing_assets=True,
226
+ for trade_dto in service.validated_trades:
227
+ instrument = Instrument.objects.get(id=trade_dto.underlying_instrument)
228
+ currency_fx_rate = instrument.currency.convert(
229
+ last_effective_date, self.portfolio.currency, exact_lookup=True
206
230
  )
207
- self.portfolio.change_at_date(to_date, base_assets=self.base_assets, force_recompute_weighting=True)
208
-
209
- def revert_trades(self):
210
- self.trades.exclude(status=Trade.Status.EXECUTED).update(status=Trade.Status.EXECUTED)
211
- for trade in self.trades.all():
212
- trade.revert()
231
+ trade = Trade(
232
+ underlying_instrument=instrument,
233
+ transaction_subtype=Trade.Type.BUY if trade_dto.delta_weight > 0 else Trade.Type.SELL,
234
+ currency=instrument.currency,
235
+ value_date=last_effective_date,
236
+ transaction_date=self.trade_date,
237
+ trade_proposal=self,
238
+ portfolio=self.portfolio,
239
+ weighting=trade_dto.delta_weight,
240
+ status=Trade.Status.DRAFT,
241
+ currency_fx_rate=currency_fx_rate,
242
+ )
243
+ trade.shares = self.estimate_shares(trade)
213
244
  trade.save()
214
- if previous_trade_proposal := self.previous_trade_proposal:
215
- previous_trade_proposal.apply_trades()
216
245
 
217
- def create_or_update_trades(
218
- self, target_portfolio: PortfolioDTO = None, effective_portfolio: PortfolioDTO = None, reset: bool = False
219
- ):
220
- """
221
- This function talk to the trading service layer in order to generate a list of valid trades to attach to the proposal
246
+ def replay(self):
247
+ trade_proposal = self
248
+ while trade_proposal and trade_proposal.status == TradeProposal.Status.APPROVED:
249
+ logger.info(f"Replaying trade proposal {self}")
250
+ trade_proposal.portfolio.assets.filter(
251
+ date=trade_proposal.trade_date
252
+ ).delete() # we delete the existing position and we reapply the trade proposal
253
+ if not trade_proposal.portfolio.assets.filter(date=trade_proposal.trade_date).exists():
254
+ if trade_proposal.status == TradeProposal.Status.APPROVED:
255
+ trade_proposal.revert()
256
+ if trade_proposal.status == TradeProposal.Status.DRAFT:
257
+ trade_proposal.submit()
258
+ if trade_proposal.status == TradeProposal.Status.SUBMIT:
259
+ trade_proposal.approve()
260
+ trade_proposal.save()
261
+ next_trade_proposal = trade_proposal.next_trade_proposal
262
+ next_trade_date = (
263
+ next_trade_proposal.trade_date - timedelta(days=1) if next_trade_proposal else date.today()
264
+ )
265
+ overriding_trade_proposal = trade_proposal.portfolio.batch_portfolio(
266
+ trade_proposal.trade_date, next_trade_date
267
+ )
268
+ trade_proposal = overriding_trade_proposal or next_trade_proposal
222
269
 
223
- Args:
224
- target_portfolio: The target portfolio that the trades needs to execute to. Absence of position means a sell
225
- effective_portfolio: The current or effective portfolio to derivative effective weight from. Absence of position means a buy
226
- reset: If true, delete the current attached trades
227
- """
228
- # if the target portfolio is not provided, we try to build it
229
- if (
230
- not target_portfolio
231
- and (assets := self.model_portfolio.assets.filter(date__lte=self.trade_date)).exists()
232
- and (latest_pos := assets.latest("date"))
233
- ):
234
- target_portfolio = self.model_portfolio._build_dto(latest_pos.date)
235
-
236
- # if the effective portfolio is not provided, we try to build it
237
- if (
238
- not effective_portfolio
239
- and (assets := self.portfolio.assets.filter(date__lte=self.trade_date)).exists()
240
- and (latest_pos := assets.latest("date"))
241
- ):
242
- effective_portfolio = self.portfolio._build_dto(latest_pos.date)
243
- # Build trades DTO from the attached trades
244
- trade_batch = self._build_dto()
245
- if target_portfolio or effective_portfolio or trade_batch:
246
- service = TradingService(
247
- self.trade_date,
248
- effective_portfolio=effective_portfolio,
249
- target_portfolio=target_portfolio,
250
- trades_batch=trade_batch,
270
+ def estimate_shares(self, trade: Trade) -> Decimal | None:
271
+ if not self.portfolio.only_weighting and (quote := trade.underlying_quote_price):
272
+ trade_total_value_fx_portfolio = (
273
+ self.portfolio.get_total_asset_value(trade.value_date) * trade._target_weight
251
274
  )
252
- # with suppress(ValidationError):
253
- # Normalize the trades and validate it
254
- service.normalize()
255
- service.is_valid()
256
- if reset:
257
- self.trades.all().delete()
258
- for trade_dto in service.validated_trades:
259
- instrument = Instrument.objects.get(id=trade_dto.underlying_instrument)
260
- t, c = Trade.objects.update_or_create(
261
- underlying_instrument=instrument,
262
- currency=instrument.currency,
263
- transaction_date=self.trade_date,
264
- trade_proposal=self,
265
- portfolio=self.portfolio,
266
- defaults={
267
- "shares": trade_dto.target_shares,
268
- "weighting": trade_dto.delta_weight,
269
- "status": Trade.Status.DRAFT,
270
- "currency_fx_rate": instrument.currency.convert(self.trade_date, self.portfolio.currency),
271
- },
272
- )
273
-
274
- # End tools methods
275
+ price_fx_portfolio = quote.net_value * trade.currency_fx_rate
276
+ if price_fx_portfolio:
277
+ return trade_total_value_fx_portfolio / price_fx_portfolio
275
278
 
276
279
  # Start FSM logics
277
280
 
@@ -295,7 +298,10 @@ class TradeProposal(RiskCheckMixin, WBModel):
295
298
  },
296
299
  )
297
300
  def submit(self, by=None, description=None, **kwargs):
298
- self.trades.update(status=Trade.Status.SUBMIT)
301
+ self.trades.update(comment="", status=Trade.Status.DRAFT)
302
+ for trade in self.trades.all():
303
+ trade.submit()
304
+ trade.save()
299
305
  self.evaluate_active_rules(
300
306
  self.trade_date, self.validated_trading_service.target_portfolio, asynchronously=True
301
307
  )
@@ -348,17 +354,28 @@ class TradeProposal(RiskCheckMixin, WBModel):
348
354
  )
349
355
  },
350
356
  )
351
- def approve(self, by=None, description=None, **kwargs):
352
- apply_trades_proposal_as_task.delay(self.id)
357
+ def approve(self, by=None, description=None, synchronous=False, **kwargs):
358
+ # We validate trade which will create or update the initial asset positions
359
+ if not self.portfolio.can_be_rebalanced:
360
+ raise ValueError("Non-Rebalanceable portfolio cannot be traded manually.")
361
+ self.trades.update(status=Trade.Status.SUBMIT)
362
+ self.portfolio.assets.filter(date=self.trade_date).delete() # we delete position to avoid having leftovers
363
+ for trade in self.trades.all():
364
+ trade.execute()
365
+ trade.save()
366
+ self.portfolio.change_at_date(self.trade_date)
367
+ # replay_as_task.delay(self.id)
353
368
 
354
369
  def can_approve(self):
355
370
  errors = dict()
371
+ if not self.portfolio.can_be_rebalanced:
372
+ errors["non_field_errors"] = "The portfolio does not allow manual rebalanced"
356
373
  if self.trades.exclude(status=Trade.Status.SUBMIT).exists():
357
374
  errors["non_field_errors"] = "At least one trade needs to be submitted to be able to approve this proposal"
358
- if not self.portfolio.is_manageable:
359
- errors[
360
- "portfolio"
361
- ] = "The portfolio needs to be a model portfolio in order to approve this trade proposal manually"
375
+ if not self.portfolio.can_be_rebalanced:
376
+ errors["portfolio"] = (
377
+ "The portfolio needs to be a model portfolio in order to approve this trade proposal manually"
378
+ )
362
379
  if self.has_assigned_active_rules and not self.has_all_check_completed_and_succeed:
363
380
  errors["non_field_errors"] = "The pre trades rules did not passed successfully"
364
381
  return errors
@@ -421,10 +438,7 @@ class TradeProposal(RiskCheckMixin, WBModel):
421
438
  self.checks.delete()
422
439
 
423
440
  def can_backtodraft(self):
424
- errors = dict()
425
- if self.trades.exclude(status=Trade.Status.SUBMIT).exists():
426
- errors["non_field_errors"] = "All trades need to be submitted before reverting back to draft"
427
- return errors
441
+ pass
428
442
 
429
443
  @transition(
430
444
  field=status,
@@ -448,16 +462,17 @@ class TradeProposal(RiskCheckMixin, WBModel):
448
462
  def revert(self, **kwargs):
449
463
  with suppress(KeyError):
450
464
  del self.__dict__["validated_trading_service"]
451
- revert_trade_proposal_as_task.delay(self.id, **kwargs)
465
+ for trade in self.trades.filter(status=Trade.Status.EXECUTED):
466
+ trade.revert()
467
+ trade.save()
468
+ # replay_as_task.delay(self.id)
452
469
 
453
470
  def can_revert(self):
454
471
  errors = dict()
455
- if self.trades.exclude(status=Trade.Status.EXECUTED).exists():
456
- errors["non_field_errors"] = "All trades need to be executed before reverting"
457
- if not self.portfolio.is_manageable:
458
- errors[
459
- "portfolio"
460
- ] = "The portfolio needs to be a model portfolio in order to revert this trade proposal manually"
472
+ if not self.portfolio.can_be_rebalanced:
473
+ errors["portfolio"] = (
474
+ "The portfolio needs to be a model portfolio in order to revert this trade proposal manually"
475
+ )
461
476
  return errors
462
477
 
463
478
  # End FSM logics
@@ -478,25 +493,8 @@ class TradeProposal(RiskCheckMixin, WBModel):
478
493
  def get_representation_label_key(cls) -> str:
479
494
  return "{{_portfolio.name}} ({{trade_date}})"
480
495
 
481
- class Meta:
482
- verbose_name = "Trade Proposal"
483
- verbose_name_plural = "Trade Proposals"
484
- unique_together = ["portfolio", "trade_date"]
485
-
486
-
487
- @shared_task(queue="portfolio")
488
- def apply_trades_proposal_as_task(trade_proposal_id):
489
- trade_proposal = TradeProposal.objects.get(id=trade_proposal_id)
490
- trade_proposal.apply_trades()
491
-
492
496
 
493
497
  @shared_task(queue="portfolio")
494
- def revert_trade_proposal_as_task(trade_proposal_id, **kwargs):
498
+ def replay_as_task(trade_proposal_id):
495
499
  trade_proposal = TradeProposal.objects.get(id=trade_proposal_id)
496
- trade_proposal.revert_trades()
497
-
498
-
499
- @receiver(post_save, sender="wbportfolio.TradeProposal")
500
- def post_save_trade_proposal(sender, instance, created, raw, **kwargs):
501
- if created and not raw and instance.portfolio.assets.filter(date__lte=instance.trade_date).exists():
502
- instance.create_or_update_trades(reset=True)
500
+ trade_proposal.replay()