wbportfolio 1.44.5__py2.py3-none-any.whl → 1.45.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wbportfolio might be problematic. Click here for more details.

Files changed (317) hide show
  1. wbportfolio/admin/__init__.py +1 -1
  2. wbportfolio/admin/asset.py +2 -1
  3. wbportfolio/admin/custodians.py +1 -0
  4. wbportfolio/admin/indexes.py +15 -0
  5. wbportfolio/admin/portfolio.py +12 -7
  6. wbportfolio/admin/portfolio_relationships.py +1 -0
  7. wbportfolio/admin/product_groups.py +2 -0
  8. wbportfolio/admin/products.py +2 -1
  9. wbportfolio/admin/reconciliations.py +1 -0
  10. wbportfolio/admin/registers.py +1 -0
  11. wbportfolio/admin/roles.py +1 -0
  12. wbportfolio/admin/transactions/__init__.py +1 -0
  13. wbportfolio/admin/transactions/claim.py +1 -0
  14. wbportfolio/admin/transactions/dividends.py +1 -0
  15. wbportfolio/admin/transactions/fees.py +1 -0
  16. wbportfolio/admin/transactions/rebalancing.py +26 -0
  17. wbportfolio/admin/transactions/trades.py +4 -3
  18. wbportfolio/admin/transactions/transactions.py +1 -0
  19. wbportfolio/analysis/claims.py +2 -1
  20. wbportfolio/contrib/company_portfolio/models.py +3 -6
  21. wbportfolio/contrib/company_portfolio/tests/conftest.py +0 -12
  22. wbportfolio/contrib/company_portfolio/tests/test_models.py +1 -0
  23. wbportfolio/defaults/fees/default.py +1 -0
  24. wbportfolio/factories/__init__.py +1 -7
  25. wbportfolio/factories/adjustments.py +1 -0
  26. wbportfolio/factories/assets.py +13 -7
  27. wbportfolio/factories/claim.py +1 -0
  28. wbportfolio/factories/custodians.py +1 -0
  29. wbportfolio/factories/dividends.py +1 -0
  30. wbportfolio/factories/fees.py +1 -0
  31. wbportfolio/factories/indexes.py +1 -0
  32. wbportfolio/factories/portfolio_cash_flow.py +1 -0
  33. wbportfolio/factories/portfolio_cash_targets.py +1 -0
  34. wbportfolio/factories/portfolio_swing_pricings.py +1 -0
  35. wbportfolio/factories/portfolios.py +3 -0
  36. wbportfolio/factories/product_groups.py +1 -0
  37. wbportfolio/factories/products.py +1 -0
  38. wbportfolio/factories/rebalancing.py +23 -0
  39. wbportfolio/factories/reconciliations.py +1 -0
  40. wbportfolio/factories/roles.py +1 -0
  41. wbportfolio/factories/trades.py +1 -0
  42. wbportfolio/factories/transactions.py +1 -0
  43. wbportfolio/fdm/tasks.py +1 -0
  44. wbportfolio/filters/__init__.py +1 -1
  45. wbportfolio/filters/assets.py +8 -9
  46. wbportfolio/filters/assets_and_net_new_money_progression.py +1 -0
  47. wbportfolio/filters/custodians.py +1 -0
  48. wbportfolio/filters/esg.py +1 -0
  49. wbportfolio/filters/performances.py +7 -6
  50. wbportfolio/filters/portfolios.py +21 -1
  51. wbportfolio/filters/positions.py +1 -0
  52. wbportfolio/filters/products.py +1 -0
  53. wbportfolio/filters/roles.py +1 -0
  54. wbportfolio/filters/signals.py +1 -0
  55. wbportfolio/filters/transactions/claim.py +1 -0
  56. wbportfolio/filters/transactions/fees.py +1 -0
  57. wbportfolio/filters/transactions/trades.py +2 -1
  58. wbportfolio/filters/transactions/transactions.py +1 -0
  59. wbportfolio/import_export/backends/ubs/mixin.py +1 -0
  60. wbportfolio/import_export/backends/wbfdm/adjustment.py +1 -0
  61. wbportfolio/import_export/handlers/asset_position.py +11 -13
  62. wbportfolio/import_export/handlers/fees.py +1 -0
  63. wbportfolio/import_export/handlers/portfolio_cash_flow.py +1 -0
  64. wbportfolio/import_export/handlers/trade.py +1 -0
  65. wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +1 -0
  66. wbportfolio/import_export/parsers/jpmorgan/fees.py +1 -0
  67. wbportfolio/import_export/parsers/jpmorgan/strategy.py +5 -4
  68. wbportfolio/import_export/parsers/jpmorgan/valuation.py +1 -0
  69. wbportfolio/import_export/parsers/leonteq/customer_trade.py +1 -0
  70. wbportfolio/import_export/parsers/leonteq/equity.py +13 -12
  71. wbportfolio/import_export/parsers/leonteq/fees.py +1 -0
  72. wbportfolio/import_export/parsers/leonteq/trade.py +1 -0
  73. wbportfolio/import_export/parsers/leonteq/valuation.py +1 -0
  74. wbportfolio/import_export/parsers/natixis/customer_trade.py +1 -0
  75. wbportfolio/import_export/parsers/natixis/d1_customer_trade.py +1 -0
  76. wbportfolio/import_export/parsers/natixis/d1_equity.py +3 -2
  77. wbportfolio/import_export/parsers/natixis/d1_fees.py +1 -0
  78. wbportfolio/import_export/parsers/natixis/d1_trade.py +1 -0
  79. wbportfolio/import_export/parsers/natixis/d1_valuation.py +1 -0
  80. wbportfolio/import_export/parsers/natixis/equity.py +5 -5
  81. wbportfolio/import_export/parsers/natixis/trade.py +1 -0
  82. wbportfolio/import_export/parsers/natixis/utils.py +8 -7
  83. wbportfolio/import_export/parsers/sg_lux/custodian_positions.py +1 -0
  84. wbportfolio/import_export/parsers/sg_lux/customer_trade.py +1 -0
  85. wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +2 -1
  86. wbportfolio/import_export/parsers/sg_lux/customer_trade_slk.py +2 -1
  87. wbportfolio/import_export/parsers/sg_lux/customer_trade_without_pw.py +1 -0
  88. wbportfolio/import_export/parsers/sg_lux/equity.py +7 -8
  89. wbportfolio/import_export/parsers/sg_lux/portfolio_cash_flow.py +1 -0
  90. wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py +1 -0
  91. wbportfolio/import_export/parsers/sg_lux/registers.py +2 -1
  92. wbportfolio/import_export/parsers/societe_generale/customer_trade.py +1 -0
  93. wbportfolio/import_export/parsers/societe_generale/strategy.py +8 -9
  94. wbportfolio/import_export/parsers/societe_generale/valuation.py +1 -0
  95. wbportfolio/import_export/parsers/tellco/equity.py +5 -4
  96. wbportfolio/import_export/parsers/ubs/api/asset_position.py +15 -14
  97. wbportfolio/import_export/parsers/ubs/api/fees.py +1 -0
  98. wbportfolio/import_export/parsers/ubs/customer_trade.py +1 -0
  99. wbportfolio/import_export/parsers/ubs/equity.py +3 -2
  100. wbportfolio/import_export/parsers/ubs/historical_customer_trade.py +1 -0
  101. wbportfolio/import_export/parsers/ubs/valuation.py +1 -0
  102. wbportfolio/import_export/parsers/vontobel/asset_position.py +19 -19
  103. wbportfolio/import_export/parsers/vontobel/customer_trade.py +1 -0
  104. wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +1 -0
  105. wbportfolio/import_export/parsers/vontobel/management_fees.py +1 -0
  106. wbportfolio/import_export/parsers/vontobel/performance_fees.py +1 -0
  107. wbportfolio/import_export/parsers/vontobel/trade.py +1 -0
  108. wbportfolio/import_export/parsers/vontobel/valuation_api.py +23 -0
  109. wbportfolio/import_export/resources/assets.py +4 -3
  110. wbportfolio/import_export/resources/trades.py +1 -0
  111. wbportfolio/metric/backends/base.py +1 -0
  112. wbportfolio/metric/backends/portfolio_base.py +1 -0
  113. wbportfolio/metric/backends/portfolio_esg.py +1 -0
  114. wbportfolio/metric/tests/test_portfolio_base.py +1 -0
  115. wbportfolio/migrations/0052_remove_cash_instrument_ptr_and_more.py +1 -131
  116. wbportfolio/migrations/0067_assetposition_unique_asset_position.py +1 -1
  117. wbportfolio/migrations/0070_remove_assetposition_unique_asset_position_and_more.py +1 -1
  118. wbportfolio/migrations/0073_remove_product_price_computation_and_more.py +407 -0
  119. wbportfolio/models/__init__.py +0 -5
  120. wbportfolio/models/adjustments.py +8 -2
  121. wbportfolio/models/asset.py +117 -98
  122. wbportfolio/models/graphs/portfolio.py +161 -0
  123. wbportfolio/models/graphs/utils.py +83 -0
  124. wbportfolio/models/indexes.py +2 -13
  125. wbportfolio/models/mixins/instruments.py +28 -8
  126. wbportfolio/models/portfolio.py +538 -332
  127. wbportfolio/models/portfolio_cash_flow.py +1 -0
  128. wbportfolio/models/portfolio_relationship.py +6 -2
  129. wbportfolio/models/product_groups.py +3 -2
  130. wbportfolio/models/products.py +3 -17
  131. wbportfolio/models/reconciliations/account_reconciliation_lines.py +1 -0
  132. wbportfolio/models/reconciliations/account_reconciliations.py +1 -0
  133. wbportfolio/models/registers.py +1 -0
  134. wbportfolio/models/transactions/__init__.py +1 -0
  135. wbportfolio/models/transactions/claim.py +8 -8
  136. wbportfolio/models/transactions/dividends.py +1 -0
  137. wbportfolio/models/transactions/fees.py +1 -0
  138. wbportfolio/models/transactions/rebalancing.py +153 -0
  139. wbportfolio/models/transactions/trade_proposals.py +153 -155
  140. wbportfolio/models/transactions/trades.py +48 -40
  141. wbportfolio/models/transactions/transactions.py +6 -12
  142. wbportfolio/models/utils.py +1 -0
  143. wbportfolio/pms/analytics/__init__.py +0 -0
  144. wbportfolio/pms/analytics/portfolio.py +28 -0
  145. wbportfolio/pms/trading/handler.py +13 -16
  146. wbportfolio/pms/typing.py +13 -29
  147. wbportfolio/rebalancing/__init__.py +0 -0
  148. wbportfolio/rebalancing/base.py +16 -0
  149. wbportfolio/rebalancing/decorators.py +17 -0
  150. wbportfolio/rebalancing/models/__init__.py +3 -0
  151. wbportfolio/rebalancing/models/composite.py +31 -0
  152. wbportfolio/rebalancing/models/equally_weighted.py +21 -0
  153. wbportfolio/rebalancing/models/model_portfolio.py +35 -0
  154. wbportfolio/reports/monthly_position_report.py +1 -1
  155. wbportfolio/risk_management/backends/accounts.py +7 -6
  156. wbportfolio/risk_management/backends/controversy_portfolio.py +1 -0
  157. wbportfolio/risk_management/backends/exposure_portfolio.py +1 -0
  158. wbportfolio/risk_management/backends/instrument_list_portfolio.py +1 -0
  159. wbportfolio/risk_management/backends/liquidity_risk.py +1 -0
  160. wbportfolio/risk_management/backends/liquidity_stress_instrument.py +1 -0
  161. wbportfolio/risk_management/backends/mixins.py +1 -0
  162. wbportfolio/risk_management/backends/product_integrity.py +6 -1
  163. wbportfolio/risk_management/backends/stop_loss_instrument.py +1 -0
  164. wbportfolio/risk_management/backends/stop_loss_portfolio.py +1 -0
  165. wbportfolio/risk_management/backends/ucits_portfolio.py +12 -5
  166. wbportfolio/risk_management/tests/test_accounts.py +1 -0
  167. wbportfolio/risk_management/tests/test_controversy_portfolio.py +1 -0
  168. wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -0
  169. wbportfolio/risk_management/tests/test_instrument_list_portfolio.py +1 -0
  170. wbportfolio/risk_management/tests/test_liquidity_risk.py +1 -0
  171. wbportfolio/risk_management/tests/test_product_integrity.py +1 -0
  172. wbportfolio/risk_management/tests/test_stop_loss_instrument.py +1 -0
  173. wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -0
  174. wbportfolio/risk_management/tests/test_ucits_portfolio.py +2 -1
  175. wbportfolio/serializers/__init__.py +5 -5
  176. wbportfolio/serializers/adjustments.py +1 -0
  177. wbportfolio/serializers/assets.py +18 -19
  178. wbportfolio/serializers/custodians.py +1 -0
  179. wbportfolio/serializers/portfolio_cash_flow.py +1 -0
  180. wbportfolio/serializers/portfolio_cash_targets.py +1 -0
  181. wbportfolio/serializers/portfolio_relationship.py +1 -0
  182. wbportfolio/serializers/portfolio_swing_pricing.py +1 -0
  183. wbportfolio/serializers/portfolios.py +61 -40
  184. wbportfolio/serializers/positions.py +1 -0
  185. wbportfolio/serializers/product_group.py +1 -0
  186. wbportfolio/serializers/products.py +4 -7
  187. wbportfolio/serializers/rebalancing.py +57 -0
  188. wbportfolio/serializers/reconciliations.py +2 -1
  189. wbportfolio/serializers/registers.py +1 -0
  190. wbportfolio/serializers/roles.py +1 -0
  191. wbportfolio/serializers/signals.py +10 -15
  192. wbportfolio/serializers/transactions/__init__.py +1 -1
  193. wbportfolio/serializers/transactions/claim.py +1 -0
  194. wbportfolio/serializers/transactions/fees.py +1 -0
  195. wbportfolio/serializers/transactions/trade_proposals.py +85 -0
  196. wbportfolio/serializers/transactions/trades.py +9 -51
  197. wbportfolio/serializers/transactions/transactions.py +4 -3
  198. wbportfolio/tasks.py +1 -78
  199. wbportfolio/tests/conftest.py +6 -13
  200. wbportfolio/tests/models/test_account_reconciliation.py +2 -0
  201. wbportfolio/tests/models/test_assets.py +27 -19
  202. wbportfolio/tests/models/test_customer_trades.py +1 -0
  203. wbportfolio/tests/models/test_imports.py +5 -1
  204. wbportfolio/tests/models/test_merge.py +5 -4
  205. wbportfolio/tests/models/test_portfolio_cash_flow.py +8 -6
  206. wbportfolio/tests/models/test_portfolios.py +619 -154
  207. wbportfolio/tests/models/test_product_groups.py +1 -0
  208. wbportfolio/tests/models/test_products.py +6 -3
  209. wbportfolio/tests/models/test_roles.py +1 -0
  210. wbportfolio/tests/models/test_splits.py +1 -0
  211. wbportfolio/tests/models/transactions/test_claim.py +1 -0
  212. wbportfolio/tests/models/transactions/test_fees.py +1 -0
  213. wbportfolio/tests/models/transactions/test_rebalancing.py +81 -0
  214. wbportfolio/tests/models/transactions/test_trades.py +1 -0
  215. wbportfolio/tests/models/utils.py +1 -0
  216. wbportfolio/tests/pms/__init__.py +0 -0
  217. wbportfolio/tests/pms/test_analytics.py +35 -0
  218. wbportfolio/tests/rebalancing/__init__.py +0 -0
  219. wbportfolio/tests/rebalancing/test_models.py +127 -0
  220. wbportfolio/tests/serializers/test_claims.py +1 -0
  221. wbportfolio/tests/signals.py +1 -7
  222. wbportfolio/tests/tests.py +2 -0
  223. wbportfolio/tests/viewsets/test_assets.py +1 -0
  224. wbportfolio/tests/viewsets/test_performances.py +1 -0
  225. wbportfolio/tests/viewsets/test_products.py +1 -0
  226. wbportfolio/tests/viewsets/transactions/test_claims.py +1 -0
  227. wbportfolio/urls.py +26 -12
  228. wbportfolio/viewsets/__init__.py +2 -5
  229. wbportfolio/viewsets/adjustments.py +1 -0
  230. wbportfolio/viewsets/assets.py +62 -51
  231. wbportfolio/viewsets/assets_and_net_new_money_progression.py +1 -0
  232. wbportfolio/viewsets/charts/assets.py +3 -1
  233. wbportfolio/viewsets/configs/buttons/__init__.py +1 -1
  234. wbportfolio/viewsets/configs/buttons/assets.py +1 -0
  235. wbportfolio/viewsets/configs/buttons/custodians.py +1 -0
  236. wbportfolio/viewsets/configs/buttons/mixins.py +1 -20
  237. wbportfolio/viewsets/configs/buttons/portfolios.py +90 -76
  238. wbportfolio/viewsets/configs/buttons/signals.py +1 -0
  239. wbportfolio/viewsets/configs/buttons/trades.py +1 -0
  240. wbportfolio/viewsets/configs/display/__init__.py +2 -1
  241. wbportfolio/viewsets/configs/display/adjustments.py +1 -0
  242. wbportfolio/viewsets/configs/display/assets.py +7 -6
  243. wbportfolio/viewsets/configs/display/claim.py +1 -0
  244. wbportfolio/viewsets/configs/display/portfolios.py +127 -79
  245. wbportfolio/viewsets/configs/display/product_performance.py +1 -0
  246. wbportfolio/viewsets/configs/display/rebalancing.py +27 -0
  247. wbportfolio/viewsets/configs/display/trade_proposals.py +7 -4
  248. wbportfolio/viewsets/configs/display/trades.py +75 -42
  249. wbportfolio/viewsets/configs/endpoints/__init__.py +3 -1
  250. wbportfolio/viewsets/configs/endpoints/assets.py +12 -0
  251. wbportfolio/viewsets/configs/endpoints/claim.py +1 -0
  252. wbportfolio/viewsets/configs/endpoints/portfolios.py +23 -7
  253. wbportfolio/viewsets/configs/endpoints/rebalancing.py +6 -0
  254. wbportfolio/viewsets/configs/endpoints/reconciliations.py +1 -0
  255. wbportfolio/viewsets/configs/endpoints/trade_proposals.py +1 -0
  256. wbportfolio/viewsets/configs/endpoints/trades.py +1 -0
  257. wbportfolio/viewsets/configs/menu/adjustments.py +1 -0
  258. wbportfolio/viewsets/configs/menu/assets.py +1 -0
  259. wbportfolio/viewsets/configs/menu/fees.py +1 -0
  260. wbportfolio/viewsets/configs/menu/portfolio_cash_flow.py +1 -0
  261. wbportfolio/viewsets/configs/menu/portfolios.py +4 -2
  262. wbportfolio/viewsets/configs/menu/positions.py +1 -0
  263. wbportfolio/viewsets/configs/menu/roles.py +1 -0
  264. wbportfolio/viewsets/configs/menu/transactions.py +1 -0
  265. wbportfolio/viewsets/configs/previews/portfolios.py +1 -6
  266. wbportfolio/viewsets/configs/titles/__init__.py +1 -1
  267. wbportfolio/viewsets/configs/titles/assets.py +1 -0
  268. wbportfolio/viewsets/configs/titles/fees.py +1 -0
  269. wbportfolio/viewsets/configs/titles/instrument_prices.py +1 -0
  270. wbportfolio/viewsets/configs/titles/portfolios.py +13 -11
  271. wbportfolio/viewsets/configs/titles/roles.py +1 -0
  272. wbportfolio/viewsets/configs/titles/trades.py +1 -0
  273. wbportfolio/viewsets/configs/titles/transactions.py +1 -0
  274. wbportfolio/viewsets/custodians.py +1 -0
  275. wbportfolio/viewsets/esg.py +1 -0
  276. wbportfolio/viewsets/mixins.py +1 -0
  277. wbportfolio/viewsets/portfolio_cash_flow.py +1 -0
  278. wbportfolio/viewsets/portfolio_cash_targets.py +1 -0
  279. wbportfolio/viewsets/portfolio_relationship.py +1 -0
  280. wbportfolio/viewsets/portfolio_swing_pricing.py +1 -0
  281. wbportfolio/viewsets/portfolios.py +228 -61
  282. wbportfolio/viewsets/positions.py +3 -2
  283. wbportfolio/viewsets/product_groups.py +1 -0
  284. wbportfolio/viewsets/product_performance.py +1 -0
  285. wbportfolio/viewsets/products.py +1 -0
  286. wbportfolio/viewsets/reconciliations.py +1 -0
  287. wbportfolio/viewsets/registers.py +1 -0
  288. wbportfolio/viewsets/roles.py +1 -0
  289. wbportfolio/viewsets/signals.py +1 -0
  290. wbportfolio/viewsets/transactions/__init__.py +1 -0
  291. wbportfolio/viewsets/transactions/claim.py +2 -1
  292. wbportfolio/viewsets/transactions/fees.py +1 -0
  293. wbportfolio/viewsets/transactions/mixins.py +1 -0
  294. wbportfolio/viewsets/transactions/rebalancing.py +31 -0
  295. wbportfolio/viewsets/transactions/trade_proposals.py +25 -5
  296. wbportfolio/viewsets/transactions/trades.py +16 -9
  297. wbportfolio/viewsets/transactions/transactions.py +1 -0
  298. {wbportfolio-1.44.5.dist-info → wbportfolio-1.45.1.dist-info}/METADATA +4 -1
  299. wbportfolio-1.45.1.dist-info/RECORD +521 -0
  300. wbportfolio/admin/synchronization/__init__.py +0 -2
  301. wbportfolio/admin/synchronization/admin.py +0 -114
  302. wbportfolio/admin/synchronization/portfolio_synchronization.py +0 -18
  303. wbportfolio/admin/synchronization/price_computation.py +0 -21
  304. wbportfolio/defaults/portfolio/default_rebalancing.py +0 -45
  305. wbportfolio/factories/pytest_utils.py +0 -121
  306. wbportfolio/factories/synchronization.py +0 -40
  307. wbportfolio/models/synchronization/__init__.py +0 -3
  308. wbportfolio/models/synchronization/portfolio_synchronization.py +0 -292
  309. wbportfolio/models/synchronization/price_computation.py +0 -200
  310. wbportfolio/models/synchronization/synchronization.py +0 -188
  311. wbportfolio/serializers/synchronization.py +0 -18
  312. wbportfolio/tests/models/test_synchronization.py +0 -617
  313. wbportfolio/viewsets/synchronization.py +0 -25
  314. wbportfolio-1.44.5.dist-info/RECORD +0 -508
  315. /wbportfolio/{defaults/portfolio → models/graphs}/__init__.py +0 -0
  316. {wbportfolio-1.44.5.dist-info → wbportfolio-1.45.1.dist-info}/WHEEL +0 -0
  317. {wbportfolio-1.44.5.dist-info → wbportfolio-1.45.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,200 +0,0 @@
1
- import logging
2
- import math
3
- from datetime import date, datetime, timedelta
4
- from decimal import Decimal
5
- from typing import Any, Optional
6
-
7
- import pandas as pd
8
- from celery import shared_task
9
- from celery.canvas import Signature
10
- from django.db import models
11
- from django.utils import timezone
12
- from pandas.tseries.offsets import BDay
13
- from wbfdm.models import Instrument
14
- from wbfdm.models.instruments.instrument_prices import InstrumentPrice
15
-
16
- from .synchronization import SynchronizationTask
17
-
18
-
19
- class PriceComputation(SynchronizationTask):
20
- @property
21
- def instruments(self):
22
- for instrument in Instrument.active_objects.filter(
23
- models.Q(id__in=self.products.values("id")) | models.Q(id__in=self.indexes.values("id"))
24
- ):
25
- yield instrument
26
-
27
- def compute(
28
- self,
29
- instrument: models.Model,
30
- sync_date: date,
31
- task_execution_datetime: Optional[datetime] = None,
32
- override_execution_datetime_validity: Optional[bool] = False,
33
- **kwargs: Any,
34
- ):
35
- """
36
- Given positions at t and t-1, we compute the performance and estimate the instrument price at t
37
- If a price already exists at that date and is estimated already, we update it. If no price exists, we create it as estimated.
38
- Otherwise, we don't do anything to protect imported real prices.
39
- :param instrument: The instrument to compute the new price from
40
- :param sync_date: The date at which we need to compute the new price
41
- :param task_execution_datetime: An optional datetime specifying at which time this task was initially executed.
42
- :param override_execution_datetime_validity: If true, we don't valide `task_execution_datetime`
43
- :param kwargs: keyword arguments
44
- """
45
- if not task_execution_datetime:
46
- task_execution_datetime = timezone.now()
47
-
48
- if (
49
- (self.is_valid_date(task_execution_datetime) or override_execution_datetime_validity)
50
- and instrument.is_active_at_date(sync_date)
51
- and not sync_date.weekday() in [5, 6]
52
- ):
53
- price_data = self._import_method(instrument, sync_date, **kwargs)
54
- if (
55
- price_data
56
- and (_instrument := price_data.pop("instrument", None))
57
- and (_date := price_data.pop("date", None))
58
- ):
59
- calculated = price_data.pop("calculated", True)
60
- try:
61
- price = InstrumentPrice.objects.get(instrument=_instrument, date=_date, calculated=calculated)
62
- for k, v in price_data.items():
63
- setattr(price, k, v)
64
- price.save()
65
- except InstrumentPrice.DoesNotExist:
66
- price = InstrumentPrice.objects.create(
67
- instrument=_instrument, date=_date, calculated=calculated, **price_data
68
- )
69
-
70
- price.save() # trigger explicitly save logic as update_or_create doesn't
71
- if (
72
- _date == _instrument.prices.latest("date").date
73
- ): # if price date is the latest instrument price date, we recomputet the last valuation data
74
- _instrument.update_last_valuation_date()
75
- else:
76
- logging.info(
77
- f"Price Computation invalid: {str(instrument)} price computation with {self.name} was triggered for {sync_date} but date not valid for crontab schedule {str(self.crontab)}"
78
- )
79
-
80
- def compute_price_as_task_si(self, instrument: models.Model, sync_date: date, **kwargs: Any) -> Signature:
81
- """
82
- Utility function that returns the signature of the compute method
83
- """
84
- return compute_price_as_task.si(self.id, instrument.id, sync_date, **kwargs)
85
-
86
- def _tasks_signature(
87
- self, sync_date: Optional[date] = None, to_date: Optional[date] = None, **kwargs: Any
88
- ) -> Signature:
89
- """
90
- Gather all tasks that needs to run under this synchronization job as a list of celery signatures.
91
- This method is expected to be implemented at each inheriting class.
92
- :param args: list
93
- :param kwargs: dict
94
- :return: list[signature]
95
- """
96
- if not to_date:
97
- to_date = date.today()
98
- for instrument in self.instruments:
99
- # Get latest valuation date + 1 Bday if not given by the key word arguments
100
- instrument_sync_dates = []
101
- if sync_date:
102
- instrument_sync_dates = [sync_date]
103
- elif not sync_date and instrument.prices.exists() and (last_price := instrument.prices.latest("date")):
104
- instrument_sync_dates = map(
105
- lambda x: x.date(),
106
- pd.date_range(
107
- max(last_price.date, to_date - timedelta(days=7)),
108
- to_date,
109
- freq="B",
110
- inclusive="left",
111
- ),
112
- )
113
- for instrument_sync_date in instrument_sync_dates:
114
- if instrument.is_active_at_date(instrument_sync_date):
115
- yield compute_price_as_task.si(self.id, instrument.id, instrument_sync_date, **kwargs)
116
-
117
- @classmethod
118
- def _default_callback(cls, instrument: Instrument, val_date: date, **kwargs: Any):
119
- """
120
- Default NAV computation function. We simply compute the performance given two positions for two dates and estimate the new price
121
- based on the overall performance.
122
- TODO: If exit/buy of positions, is this function still correct?
123
- :param instrument: The instrument to compute the new price from
124
- :param val_date: The date at which we need to compute the new price
125
- :param kwargs: keyword arguments
126
- """
127
- if portfolio := instrument.portfolio:
128
- # check if the asset portfolio of this instruments exists and has positions at the synchronization date
129
- if previous_date := portfolio.get_latest_asset_position_date(val_date - BDay(1), with_estimated=True):
130
- # Get the previous valid price date before sync_date, checks if it exists and if positions are available
131
- # at that date.
132
- # If asset position exists on the previous day but not at the sync date, maybe propagation were not done, and we try it
133
- if portfolio.assets.filter(date=previous_date).exists():
134
- portfolio.propagate_or_update_assets(previous_date, val_date)
135
- if portfolio.assets.filter(date=val_date).exists():
136
- last_price = None
137
- if (
138
- last_valuation := instrument.prices.filter(date=previous_date)
139
- .order_by("calculated")
140
- .first()
141
- ):
142
- last_price = last_valuation.net_value
143
- elif not instrument.valuations.filter(date__lt=previous_date).exists():
144
- last_price = instrument.issue_price
145
- if last_price:
146
- weights = pd.DataFrame(
147
- portfolio.assets.filter(date=previous_date).values(
148
- "weighting", "date", "underlying_instrument"
149
- )
150
- )
151
- weights = weights.pivot_table(
152
- index="date", columns=["underlying_instrument"], values="weighting", aggfunc="sum"
153
- ).astype("float")
154
- weights = weights.iloc[-1, :]
155
- perfs = pd.DataFrame(
156
- portfolio.assets.filter(date__in=[previous_date, val_date]).values(
157
- "date", "price_fx_portfolio", "underlying_instrument"
158
- )
159
- )
160
- perfs = perfs.pivot_table(
161
- index="date",
162
- columns=["underlying_instrument"],
163
- values="price_fx_portfolio",
164
- aggfunc="mean",
165
- ).astype("float")
166
- perfs = perfs / perfs.shift(1, axis=0) - 1.0
167
- perfs = perfs.fillna(0).iloc[-1, :]
168
- total_perfs = float((perfs * weights).sum())
169
- new_gross_valuation = float(last_price) * (1.0 + total_perfs)
170
- if new_gross_valuation and not math.isnan(new_gross_valuation):
171
- return {
172
- "instrument": instrument,
173
- "date": val_date,
174
- "gross_value": Decimal(new_gross_valuation),
175
- "net_value": Decimal(new_gross_valuation),
176
- }
177
-
178
- return None
179
-
180
- def __str__(self) -> str:
181
- return self.name
182
-
183
- @classmethod
184
- def get_representation_endpoint(cls) -> str:
185
- return "wbportfolio:pricecomputationrepresentation-list"
186
-
187
- @classmethod
188
- def get_representation_value_key(cls) -> str:
189
- return "id"
190
-
191
- @classmethod
192
- def get_representation_label_key(cls) -> str:
193
- return "{{name}}"
194
-
195
-
196
- @shared_task(queue="portfolio")
197
- def compute_price_as_task(price_computation_method_id: int, instrument_id: int, sync_date: date, **kwargs: Any):
198
- instrument = Instrument.objects.get(id=instrument_id)
199
- price_computation = PriceComputation.objects.get(id=price_computation_method_id)
200
- price_computation.compute(instrument, sync_date, **kwargs)
@@ -1,188 +0,0 @@
1
- import importlib
2
- from collections.abc import Callable
3
- from datetime import date, datetime, time
4
- from json import loads
5
- from typing import Any, Iterator, Optional
6
-
7
- from celery import chord, group, shared_task
8
- from celery.canvas import Signature, signature
9
- from croniter import croniter, croniter_range
10
- from django.contrib.contenttypes.models import ContentType
11
- from django.db import models
12
- from django.db.models.signals import post_save
13
- from django.dispatch import receiver
14
- from django.utils import timezone
15
- from django_celery_beat.models import CrontabSchedule, PeriodicTask, cronexp
16
-
17
-
18
- class SynchronizationTask(PeriodicTask):
19
- RELATIVE_TASK_MODULE_PATH = "wbportfolio.models.synchronization.synchronization.task"
20
-
21
- dependent_task = models.ForeignKey(
22
- "self", related_name="dependency_tasks", null=True, blank=True, on_delete=models.SET_NULL
23
- )
24
- import_path = models.CharField(max_length=512, verbose_name="Import Path", default="", blank=True)
25
-
26
- @property
27
- def cast_args(self) -> list[Any]:
28
- """
29
- transform the string args representation into list. We except this to become unnecessary when django beat move to jsonfield
30
- :return: list
31
- """
32
- return loads(self.args or "[]")
33
-
34
- @property
35
- def cast_kwargs(self) -> dict[Any, Any]:
36
- """
37
- transform the string kwargs representation into dictionary. We except this to become unnecessary when django beat move to jsonfield
38
- :return: list
39
- """
40
- return loads(self.kwargs or "{}")
41
-
42
- @property
43
- def _import_method(self) -> Callable[[Any], Any]:
44
- """
45
- If a custom task is specified, return the loaded module as callabck. Otherwise, returns the default synchronization
46
- function defined in `_default_callback
47
- :return: Callable
48
- """
49
- if import_path := self.import_path:
50
- synchronize_module = importlib.import_module(import_path)
51
- return synchronize_module.callback
52
- else:
53
- return self._default_callback
54
-
55
- def schedule_str(self, filter_daily: Optional[bool] = False) -> str:
56
- """
57
- Returns the crontab string representation. If fitler_daily is true, we cast the crontab so that the lowest frequency
58
- becomes daily.
59
- :param filter_daily: bool (optional)
60
- :return: crontab string representation
61
- """
62
- crontab_minute = cronexp(self.crontab.minute)
63
- crontab_hour = cronexp(self.crontab.hour)
64
- if filter_daily:
65
- if crontab_minute == "*":
66
- crontab_minute = "0"
67
- if crontab_hour == "*":
68
- crontab_hour = "0"
69
- return "{0} {1} {2} {3} {4}".format(
70
- crontab_minute,
71
- crontab_hour,
72
- cronexp(self.crontab.day_of_month),
73
- cronexp(self.crontab.month_of_year),
74
- cronexp(self.crontab.day_of_week),
75
- )
76
-
77
- def dates_range(self, from_date: date, to_date: date, filter_daily: Optional[bool] = False) -> Iterator[datetime]:
78
- """
79
- returns a list of valid dates given an interval and a specific crontab schedule.
80
- :param from_date: date
81
- :param to_date: date
82
- :param filter_daily: bool (optional)
83
- :return: list[date]
84
- """
85
- min_datetime = datetime.combine(from_date, time.min)
86
- max_datetime = datetime.combine(to_date, time.max)
87
- return croniter_range(min_datetime, max_datetime, self.schedule_str(filter_daily=filter_daily))
88
-
89
- def is_valid_date(self, sync_datetime: datetime) -> bool:
90
- """
91
- check wether a date is valid given the stored crontab schedule
92
- :param sync_datetime: datetime
93
- :return: bool
94
- """
95
- return croniter.match(self.schedule_str(), sync_datetime)
96
-
97
- def save(self, *args: Any, **kwargs: Any):
98
- self.task = self.RELATIVE_TASK_MODULE_PATH
99
- if not self.schedule:
100
- self.crontab, _ = CrontabSchedule.objects.get_or_create(
101
- minute="0",
102
- hour="*",
103
- day_of_week="*",
104
- day_of_month="*",
105
- month_of_year="*",
106
- )
107
- if not self.crontab:
108
- raise ValueError("Synchronization task supports only Crontab Schedule")
109
- if self.crontab.minute == "*":
110
- raise ValueError("The minimum crontab frequency supported is hourly.")
111
- super().save(*args, **kwargs)
112
-
113
- def _tasks_signature(self, *args, **kwargs: Any) -> Signature:
114
- """
115
- Gather all tasks that needs to run under this synchronization job as a list of celery signatures.
116
- This method is expected to be implemented at each inheriting class.
117
- :param args: list
118
- :param kwargs: dict
119
- :return: list[signature]
120
- """
121
- raise NotImplementedError()
122
-
123
- def _default_callback(self, *args: Any, **kwargs: Any) -> Any:
124
- """
125
- The default synchronization function executed if no custom task is provided for this synchronization object.
126
- This method is expected to be implemented at each inheriting class.
127
- :param args: list
128
- :param kwargs: dict
129
- :return: callable
130
- """
131
- raise NotImplementedError()
132
-
133
- def _get_kwargs(self) -> Any:
134
- """
135
- return the base keyword argument to be injected into the `_tasks_signature` method. Define as a standalone function
136
- in order to allow subclass definition.
137
- :return: dict
138
- """
139
- return {"task_execution_datetime": timezone.now()}
140
-
141
- def _end_task_signature(self, **kwargs: Any) -> Signature:
142
- """
143
- A synchronization object can defined a dependant task that will be executed at this end of all returned task from
144
- _tasks_signatures.
145
- This function returns the signature of this chained task.
146
- :param kwargs:
147
- :return: signature
148
- """
149
- if self.dependent_task:
150
- kwargs = {"override_execution_datetime_validity": True, **self.dependent_task.cast_kwargs, **kwargs}
151
- return signature(
152
- self.dependent_task.task, args=self.dependent_task.cast_args, kwargs=kwargs, immutable=True
153
- )
154
- return None
155
-
156
- def chord(self, **kwargs: Any) -> chord:
157
- """
158
- This function is the main entry point of the synchronization worklow. It is called from within the shared_task `task`
159
- and create the celery chord containing the list of tasks chained to the end task (if any)
160
- :param kwargs:
161
- :return: chord
162
- """
163
- kwargs = {**kwargs, **self._get_kwargs()}
164
- tasks = list(self._tasks_signature(**kwargs))
165
- if end_task := self._end_task_signature(**kwargs):
166
- return chord(tasks, end_task)
167
- return group(tasks)
168
-
169
-
170
- @receiver(post_save, sender="wbportfolio.SynchronizationTask")
171
- @receiver(post_save, sender="wbportfolio.PortfolioSynchronization")
172
- @receiver(post_save, sender="wbportfolio.PriceComputation")
173
- def post_save_synchronization_task(sender, instance: models.Model, created: bool, raw: bool, **kwargs: Any):
174
- """
175
- Ensure args attribute contains the necessary arguments to retrieve the calling job from within asynchronous task
176
- """
177
- if (created and not raw) or not instance.args:
178
- content_type = ContentType.objects.get_for_model(instance)
179
- instance.args = f'["{instance.id}", "{content_type.app_label}", "{content_type.model}"]'
180
- instance.save()
181
-
182
-
183
- @shared_task
184
- def task(synchronization_object_id: int, app_label: str, model: str, **kwargs: Any):
185
- synchronization_object = ContentType.objects.get(app_label=app_label, model=model).get_object_for_this_type(
186
- id=synchronization_object_id
187
- )
188
- synchronization_object.chord(**kwargs).apply_async()
@@ -1,18 +0,0 @@
1
- from wbcore import serializers as wb_serializers
2
- from wbportfolio.models import PortfolioSynchronization, PriceComputation
3
-
4
-
5
- class PortfolioSynchronizationRepresentationSerializer(wb_serializers.RepresentationSerializer):
6
- _detail = wb_serializers.HyperlinkField(reverse_name="wbportfolio:portfolio-detail")
7
-
8
- class Meta:
9
- model = PortfolioSynchronization
10
- fields = ("id", "name", "import_path", "_detail")
11
-
12
-
13
- class PriceComputationRepresentationSerializer(wb_serializers.RepresentationSerializer):
14
- _detail = wb_serializers.HyperlinkField(reverse_name="wbportfolio:portfolio-detail")
15
-
16
- class Meta:
17
- model = PriceComputation
18
- fields = ("id", "name", "import_path", "_detail")