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,2 +0,0 @@
1
- from .portfolio_synchronization import PortfolioSynchronizationModelAdmin
2
- from .price_computation import PriceComputationModelAdmin
@@ -1,114 +0,0 @@
1
- from django_celery_beat.admin import (
2
- PeriodicTaskAdmin,
3
- PeriodicTaskForm,
4
- _,
5
- loads,
6
- messages,
7
- pluralize,
8
- )
9
-
10
-
11
- class SynchronizationTaskForm(PeriodicTaskForm):
12
- def clean(self):
13
- return self.cleaned_data
14
-
15
-
16
- class SynchronizationAdmin(PeriodicTaskAdmin):
17
- form = SynchronizationTaskForm
18
- search_fields = ("name",)
19
- regtask = None
20
- readonly_fields = (
21
- "last_run_at",
22
- # 'regtask',
23
- "task",
24
- "args",
25
- # 'kwargs',
26
- "expires",
27
- "expire_seconds",
28
- "queue",
29
- "exchange",
30
- "routing_key",
31
- "priority",
32
- "headers",
33
- )
34
- fieldsets = (
35
- (
36
- None,
37
- {
38
- "fields": (
39
- "name",
40
- "task",
41
- "enabled",
42
- "description",
43
- ),
44
- "classes": ("extrapretty", "wide"),
45
- },
46
- ),
47
- (
48
- "Schedule",
49
- {
50
- "fields": ("crontab", "start_time", "last_run_at", "one_off"),
51
- "classes": ("extrapretty", "wide"),
52
- },
53
- ),
54
- (
55
- "Arguments",
56
- {
57
- "fields": ("args", "kwargs"),
58
- "classes": ("extrapretty", "wide", "collapse", "in"),
59
- },
60
- ),
61
- (
62
- "Execution Options",
63
- {
64
- "fields": ("expires", "expire_seconds", "queue", "exchange", "routing_key", "priority", "headers"),
65
- "classes": ("extrapretty", "wide", "collapse", "in"),
66
- },
67
- ),
68
- )
69
-
70
- def run_tasks(self, request, queryset):
71
- def _load_kwargs(task):
72
- kwargs = loads(task.kwargs)
73
- kwargs["override_execution_datetime_validity"] = True
74
- return kwargs
75
-
76
- self.celery_app.loader.import_default_modules()
77
-
78
- tasks = [
79
- (self.celery_app.tasks.get(task.task), loads(task.args), _load_kwargs(task), task.queue)
80
- for task in queryset
81
- ]
82
-
83
- if any(t[0] is None for t in tasks):
84
- for i, t in enumerate(tasks):
85
- if t[0] is None:
86
- break
87
-
88
- # variable "i" will be set because list "tasks" is not empty
89
- not_found_task_name = queryset[i].task
90
-
91
- self.message_user(
92
- request,
93
- _('task "{0}" not found'.format(not_found_task_name)),
94
- level=messages.ERROR,
95
- )
96
- return
97
-
98
- task_ids = [
99
- task.apply_async(args=args, kwargs=kwargs, queue=queue)
100
- if queue and len(queue)
101
- else task.apply_async(args=args, kwargs=kwargs)
102
- for task, args, kwargs, queue in tasks
103
- ]
104
- tasks_run = len(task_ids)
105
- self.message_user(
106
- request,
107
- _("{0} task{1} {2} successfully run").format(
108
- tasks_run,
109
- pluralize(tasks_run),
110
- pluralize(tasks_run, _("was,were")),
111
- ),
112
- )
113
-
114
- run_tasks.short_description = _("Run selected tasks")
@@ -1,18 +0,0 @@
1
- from django.contrib import admin
2
- from wbportfolio.models import PortfolioSynchronization
3
-
4
- from .admin import SynchronizationAdmin
5
-
6
-
7
- @admin.register(PortfolioSynchronization)
8
- class PortfolioSynchronizationModelAdmin(SynchronizationAdmin):
9
- fieldsets = (
10
- *SynchronizationAdmin.fieldsets,
11
- (
12
- "Synchronization",
13
- {
14
- "fields": ("import_path", "dependent_task", "is_automatic_validation"),
15
- "classes": ("extrapretty", "wide"),
16
- },
17
- ),
18
- )
@@ -1,21 +0,0 @@
1
- from django.contrib import admin
2
- from wbportfolio.models import PriceComputation
3
-
4
- from .admin import SynchronizationAdmin
5
-
6
-
7
- @admin.register(PriceComputation)
8
- class PriceComputationModelAdmin(SynchronizationAdmin):
9
- fieldsets = (
10
- *SynchronizationAdmin.fieldsets,
11
- (
12
- "Synchronization",
13
- {
14
- "fields": (
15
- "import_path",
16
- "dependent_task",
17
- ),
18
- "classes": ("extrapretty", "wide"),
19
- },
20
- ),
21
- )
@@ -1,45 +0,0 @@
1
- from datetime import date
2
- from decimal import Decimal
3
- from typing import Any, Dict, Optional
4
-
5
- import pandas as pd
6
- from wbportfolio.models import Portfolio
7
-
8
-
9
- def callback(
10
- portfolio: Portfolio,
11
- sync_date: date,
12
- rebalancing_freq: Optional[str] = "B",
13
- equally_weighted: Optional[bool] = False,
14
- composite: Optional[bool] = False,
15
- base_assets: Optional[Dict] = dict(),
16
- **kwargs: Any,
17
- ):
18
- """Recursively calculates the position for a portfolio
19
-
20
- Arguments:
21
- portfolio {portfolio.Portfolio} -- The Portfolio on which the assets will be computed
22
- sync_date {datetime.date} -- The date on which the assets will be computed
23
-
24
- Keyword Arguments:
25
- portfolio {portfolio.Portfolio} -- The core portfolio from which the computed position are created (default: {None})
26
- adjusted_weighting {int} -- the adjusted weight of the current level of index (default: {1})
27
- adjusted_currency_fx_rate {int} -- the adjusted currency exchange rate on the current level of index (default: {1})
28
-
29
- Yields:
30
- tuple[dict, dict] -- Two dictionaries: One with filter parameters and one with default values
31
- """
32
- assets = portfolio.assets.filter(date=sync_date)
33
- if composite:
34
- last_trade_proposals = portfolio.trade_proposals.filter(trade_date__lte=sync_date)
35
- if last_trade_proposals.exists():
36
- base_assets = last_trade_proposals.latest("trade_date").base_assets
37
- if assets.exists() and assets.filter(date=sync_date).exists():
38
- for asset in assets.all():
39
- new_weight = asset.weighting
40
- if pd.date_range(end=sync_date, periods=1, freq=rebalancing_freq)[0] == pd.Timestamp(sync_date):
41
- if equally_weighted:
42
- new_weight = Decimal(1 / assets.count())
43
- elif base_assets and (proposed_weight := base_assets.get(asset.underlying_instrument.id, None)):
44
- new_weight = proposed_weight
45
- yield asset._build_dto(), asset._build_dto(new_weight)
@@ -1,121 +0,0 @@
1
- from datetime import date
2
-
3
- import pandas as pd
4
- from django.utils import timezone
5
- from faker import Faker
6
- from pandas.tseries.offsets import BusinessMonthEnd
7
- from wbfdm.factories import (
8
- CashFactory,
9
- ClassificationFactory,
10
- ClassificationGroupFactory,
11
- EquityFactory,
12
- InstrumentFactory,
13
- )
14
- from wbportfolio.factories import (
15
- AssetPositionFactory,
16
- InstrumentPriceFactory,
17
- PortfolioFactory,
18
- PortfolioSynchronizationFactory,
19
- PriceComputationFactory,
20
- ProductFactory,
21
- )
22
- from wbportfolio.models import (
23
- InstrumentPortfolioThroughModel,
24
- PortfolioPortfolioThroughModel,
25
- )
26
-
27
- fake = Faker()
28
-
29
-
30
- def setup_product(today):
31
- product = ProductFactory.create()
32
- product.related_instruments.add(InstrumentFactory.create())
33
- primary_group = ClassificationGroupFactory.create(is_primary=True, max_depth=3)
34
- classification_parent_1 = ClassificationFactory.create(group=primary_group)
35
- classification_parent_2 = ClassificationFactory.create(group=primary_group)
36
-
37
- i1 = EquityFactory.create(classifications=[classification_parent_1.children.first().children.first()])
38
- i2 = EquityFactory.create(classifications=[classification_parent_2.children.first().children.first()])
39
- i3 = CashFactory.create()
40
- for _d in pd.date_range(
41
- today - BusinessMonthEnd(1), today + BusinessMonthEnd(0), freq="B"
42
- ): # Build a complete factsheet month
43
- _d = _d.date()
44
- InstrumentPriceFactory.create(instrument=product, date=_d)
45
- pos = [
46
- AssetPositionFactory.create(
47
- portfolio=product.portfolio,
48
- date=_d,
49
- underlying_instrument=i1,
50
- underlying_instrument_price=InstrumentPriceFactory.create(instrument=i1, date=_d, calculated=False),
51
- ),
52
- AssetPositionFactory.create(
53
- portfolio=product.portfolio,
54
- date=_d,
55
- underlying_instrument=i2,
56
- underlying_instrument_price=InstrumentPriceFactory.create(instrument=i2, date=_d, calculated=False),
57
- ),
58
- AssetPositionFactory.create(
59
- portfolio=product.portfolio,
60
- date=_d,
61
- underlying_instrument=i3,
62
- underlying_instrument_price=InstrumentPriceFactory.create(instrument=i3, date=_d, calculated=False),
63
- ),
64
- ]
65
- for p in pos:
66
- InstrumentPriceFactory.create(instrument=p.underlying_instrument, date=_d)
67
- return product
68
-
69
-
70
- class ValidTodayProductSetupMixin:
71
- def setup_method(self, method=None):
72
- product = setup_product(timezone.now().date())
73
- self.product = product
74
-
75
-
76
- class ValidTodayMultiThematicProductSetupMixin:
77
- def setup_method(self, method=None):
78
- today = date.today()
79
- main_product = ProductFactory.create(price_computation=PriceComputationFactory.create())
80
- main_product.related_instruments.add(InstrumentFactory.create())
81
-
82
- # Create two valid products that will span our portfolio
83
- product1 = setup_product(today)
84
- product2 = setup_product(today)
85
-
86
- # Create a computed portfolio (model to allow automatic sync), linked throught he primary portfolio with a Thematic relationship
87
- computed_portfolio = PortfolioFactory.create(
88
- portfolio_synchronization=PortfolioSynchronizationFactory.create(),
89
- )
90
- PortfolioPortfolioThroughModel.objects.create(
91
- portfolio=computed_portfolio, dependency_portfolio=main_product.primary_portfolio
92
- )
93
-
94
- InstrumentPortfolioThroughModel.objects.update_or_create(
95
- instrument=main_product, defaults={"portfolio": computed_portfolio}
96
- )
97
-
98
- # Loop through the date range and create an equally weighted portfolio of products and synchronize the computed portfolio and compute the estimate NAV
99
- for _d in pd.date_range(today - BusinessMonthEnd(1), today + BusinessMonthEnd(0), freq="B"):
100
- _d = _d.date()
101
- AssetPositionFactory.create(
102
- portfolio=main_product.primary_portfolio,
103
- underlying_instrument=product1,
104
- date=_d,
105
- weighting=0.5,
106
- initial_price=product1.prices.get(date=_d).net_value,
107
- )
108
- AssetPositionFactory.create(
109
- portfolio=main_product.primary_portfolio,
110
- underlying_instrument=product2,
111
- date=_d,
112
- weighting=0.5,
113
- initial_price=product2.prices.get(date=_d).net_value,
114
- )
115
- computed_portfolio.portfolio_synchronization.synchronize(
116
- computed_portfolio, _d, override_execution_datetime_validity=True
117
- )
118
- main_product.price_computation.compute(main_product, _d, override_execution_datetime_validity=True)
119
- main_product.prices.update(calculated=False)
120
- self.product = main_product
121
- self.theme_portfolio = main_product.primary_portfolio
@@ -1,40 +0,0 @@
1
- import factory
2
- from django_celery_beat.models import CrontabSchedule, PeriodicTask
3
- from wbportfolio.models import (
4
- PortfolioSynchronization,
5
- PriceComputation,
6
- SynchronizationTask,
7
- )
8
-
9
-
10
- class CrontabScheduleFactory(factory.django.DjangoModelFactory):
11
- hour = factory.Iterator(range(0, 24))
12
- minute = factory.Iterator(range(0, 60))
13
-
14
- class Meta:
15
- model = CrontabSchedule
16
-
17
-
18
- class PeriodicTaskFactory(factory.django.DjangoModelFactory):
19
- class Meta:
20
- model = PeriodicTask
21
-
22
- name = factory.Sequence(lambda n: f"Synchronization Task {n}")
23
- crontab = factory.SubFactory(CrontabScheduleFactory)
24
-
25
-
26
- class SynchronizationTaskFactory(PeriodicTaskFactory):
27
- class Meta:
28
- model = SynchronizationTask
29
-
30
-
31
- class PortfolioSynchronizationFactory(SynchronizationTaskFactory):
32
- propagate_history = False
33
-
34
- class Meta:
35
- model = PortfolioSynchronization
36
-
37
-
38
- class PriceComputationFactory(SynchronizationTaskFactory):
39
- class Meta:
40
- model = PriceComputation
@@ -1,3 +0,0 @@
1
- from .portfolio_synchronization import PortfolioSynchronization
2
- from .price_computation import PriceComputation
3
- from .synchronization import SynchronizationTask
@@ -1,292 +0,0 @@
1
- import logging
2
- from datetime import date, datetime
3
- from decimal import Decimal
4
- from typing import Any, Optional
5
-
6
- import numpy as np
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.db.models import F
12
- from django.utils import timezone
13
- from wbfdm.enums import MarketData
14
- from wbfdm.models.instruments import Instrument
15
- from wbportfolio.models import Portfolio
16
- from wbportfolio.models.transactions.trade_proposals import TradeProposal
17
- from wbportfolio.pms.typing import Portfolio as PortfolioDTO
18
- from wbportfolio.pms.typing import Position as PositionDTO
19
-
20
- from .synchronization import SynchronizationTask
21
-
22
-
23
- def convert_currency(x, val_date, other_currency):
24
- instrument = Instrument.objects.get(id=x)
25
- try:
26
- return instrument.currency.convert(val_date, other_currency)
27
- except Exception:
28
- return np.nan
29
-
30
-
31
- class PortfolioSynchronization(SynchronizationTask):
32
- is_automatic_validation = models.BooleanField(
33
- default=True,
34
- verbose_name="Automatic validation",
35
- help_text="Set to True if you want to automatically implement proposed positions",
36
- )
37
- propagate_history = models.BooleanField(
38
- default=False,
39
- verbose_name="Propagate History",
40
- help_text="If true, when the depends on portfolio changes at a certain date, this method will trigger a synchronization for each date (at the scheduled frequency) from that date to the latest valid date",
41
- )
42
-
43
- def synchronize(
44
- self,
45
- portfolio: models.Model,
46
- sync_date: date,
47
- task_execution_datetime: Optional[datetime] = None,
48
- override_execution_datetime_validity: Optional[bool] = False,
49
- post_processing: bool = True,
50
- **kwargs: Any,
51
- ):
52
- """
53
- This function compute the new portfolio composition after synchronization (returns from `_import_method`) for a
54
- given date and either update or create the portfolio or create a trade proposal given the new portfolio constituent.
55
-
56
- :param portfolio: The portfolio to synchronize the positions from
57
- :param sync_date: The date at which we need to synchronize the given portfolio
58
- :param task_execution_datetime: An optional datetime specifying at which time this task was initially executed.
59
- :param override_execution_datetime_validity: If true, we don't valide `task_execution_datetime`
60
- :param kwargs: keyword arguments
61
- """
62
-
63
- initkwargs = {**kwargs, **self.cast_kwargs}
64
- if not task_execution_datetime:
65
- task_execution_datetime = timezone.now()
66
- if portfolio.is_active_at_date(sync_date):
67
- if self.is_valid_date(task_execution_datetime) or override_execution_datetime_validity:
68
- if import_res := list(zip(*self._import_method(portfolio, sync_date, **initkwargs))):
69
- effective_positions = list(filter(lambda x: x, import_res[0]))
70
- target_positions = list(filter(lambda x: x, import_res[1]))
71
- if len(target_positions) > 0:
72
- target_portfolio = PortfolioDTO(target_positions)
73
-
74
- effective_portfolio = (
75
- PortfolioDTO(effective_positions) if len(effective_positions) > 0 else None
76
- )
77
- if self.is_automatic_validation:
78
- # We process these positions automatically
79
- portfolio.import_positions_at_date(
80
- target_portfolio, sync_date, post_processing=post_processing
81
- )
82
- else:
83
- trade_proposal, created = TradeProposal.objects.get_or_create(
84
- trade_date=sync_date,
85
- portfolio=portfolio,
86
- defaults={"comment": "Automatic rebalancing"},
87
- )
88
- trade_proposal.create_or_update_trades(
89
- target_portfolio=target_portfolio, effective_portfolio=effective_portfolio
90
- )
91
-
92
- portfolio.last_synchronization = timezone.now()
93
- portfolio.save()
94
-
95
- else:
96
- logging.info(
97
- f"Synchronization invalid: {portfolio.name} synchronization with {self.name} was triggered for {sync_date} but date not valid for crontab schedule {str(self.crontab)}"
98
- )
99
-
100
- def synchronize_as_task_si(self, portfolio: models.Model, sync_date: date, **kwargs: Any) -> Signature:
101
- """
102
- Utility function that returns the signature of the synchronize method
103
- """
104
- return synchronize_portfolio_as_task.si(self.id, portfolio.id, sync_date, **kwargs)
105
-
106
- def _tasks_signature(self, sync_date: Optional[date] = None, **kwargs: Any) -> Signature:
107
- """
108
- Gather all tasks that needs to run under this synchronization job as a list of celery signatures.
109
- This method is expected to be implemented at each inheriting class.
110
- :param args: list
111
- :param kwargs: dict
112
- :return: list[signature]
113
- """
114
- for portfolio in self.portfolios.all():
115
- portfolio_sync_dates = []
116
- if sync_date:
117
- portfolio_sync_dates = [sync_date]
118
- elif not sync_date and portfolio.assets.exists() and (latest_asset := portfolio.assets.latest("date")):
119
- portfolio_sync_dates = map(
120
- lambda x: x.date(), pd.date_range(latest_asset.date, date.today(), freq="B", inclusive="left")
121
- )
122
- for portfolio_sync_date in portfolio_sync_dates:
123
- if portfolio.is_active_at_date(portfolio_sync_date):
124
- yield synchronize_portfolio_as_task.si(self.id, portfolio.id, portfolio_sync_date, **kwargs)
125
-
126
- @classmethod
127
- def _default_callback(
128
- cls,
129
- portfolio: Portfolio,
130
- sync_date: date,
131
- portfolio_created: Optional[Portfolio] = None,
132
- adjusted_weighting: Optional[Decimal] = Decimal(1.0),
133
- adjusted_currency_fx_rate: Optional[Decimal] = Decimal(1.0),
134
- is_estimated: Optional[bool] = False,
135
- portfolio_total_value: Optional[float] = None,
136
- **kwargs: Any,
137
- ):
138
- """Recursively calculates the position for a portfolio
139
-
140
- Arguments:
141
- portfolio {portfolio.Portfolio} -- The Portfolio on which the assets will be computed
142
- sync_date {datetime.date} -- The date on which the assets will be computed
143
-
144
- Keyword Arguments:
145
- portfolio {portfolio.Portfolio} -- The core portfolio from which the computed position are created (default: {None})
146
- adjusted_weighting {int} -- the adjusted weight of the current level of index (default: {1})
147
- adjusted_currency_fx_rate {int} -- the adjusted currency exchange rate on the current level of index (default: {1})
148
-
149
- Yields:
150
- tuple[dict, dict] -- Two dictionaries: One with filter parameters and one with default values
151
- """
152
- is_root_position_estimated = False
153
- if not portfolio_created:
154
- if portfolio_created := portfolio.primary_portfolio:
155
- is_root_position_estimated = (
156
- portfolio_created.assets.filter(date=sync_date).count() == 1
157
- and portfolio_created.assets.filter(date=sync_date, is_estimated=True).count() == 1
158
- )
159
- if portfolio_created:
160
- child_positions = portfolio_created.assets.filter(date=sync_date)
161
- asset_positions = child_positions.all()
162
- # Compute the total portfolio value based on the root position child (otherwise the value is passed as
163
- # parameters in the recursion
164
- if not portfolio_total_value:
165
- portfolio_total_value = child_positions.aggregate(tv=models.Sum(F("total_value_fx_portfolio")))["tv"]
166
- if not portfolio_total_value:
167
- portfolio_total_value = portfolio_created.get_total_value(sync_date)
168
- for position in child_positions:
169
- if child_portfolio := position.underlying_instrument.portfolio:
170
- if child_portfolio.assets.filter(date=sync_date).exists() and position.weighting is not None:
171
- asset_positions = asset_positions.exclude(id=position.id)
172
- yield from cls._default_callback(
173
- portfolio,
174
- sync_date,
175
- portfolio_created=child_portfolio,
176
- adjusted_weighting=position.weighting * adjusted_weighting,
177
- portfolio_total_value=portfolio_total_value,
178
- adjusted_currency_fx_rate=position.currency_fx_rate * adjusted_currency_fx_rate,
179
- is_estimated=False
180
- if is_root_position_estimated
181
- else (is_estimated and position.is_estimated),
182
- )
183
- df = pd.DataFrame(
184
- asset_positions.values_list(
185
- "currency_fx_rate",
186
- "price",
187
- "weighting",
188
- "shares",
189
- "is_estimated",
190
- "underlying_instrument",
191
- "currency",
192
- "exchange",
193
- ),
194
- columns=[
195
- "currency_fx_rate",
196
- "price",
197
- "weighting",
198
- "shares",
199
- "is_estimated",
200
- "underlying_instrument",
201
- "currency",
202
- "exchange",
203
- ],
204
- )
205
- if not df.empty:
206
- df.currency_fx_rate = df.currency_fx_rate * adjusted_currency_fx_rate
207
- df.weighting = df.weighting * adjusted_weighting
208
-
209
- df = (
210
- df.groupby(["underlying_instrument", "currency", "exchange"], dropna=False)
211
- .agg(
212
- {
213
- "currency_fx_rate": "first",
214
- "price": "first",
215
- "weighting": "sum",
216
- "shares": "sum",
217
- "is_estimated": "first",
218
- }
219
- )
220
- .reset_index()
221
- )
222
- df[["underlying_instrument", "currency", "exchange"]] = df[
223
- ["underlying_instrument", "currency", "exchange"]
224
- ].astype("object")
225
- df[["currency_fx_rate", "price", "weighting", "shares"]] = df[
226
- ["currency_fx_rate", "price", "weighting", "shares"]
227
- ].astype("float")
228
-
229
- df["actual_currency_fx_rate"] = df.underlying_instrument.apply(
230
- lambda x: convert_currency(x, sync_date, portfolio.currency)
231
- ).astype("float")
232
- df["actual_currency_fx_rate"] = df["actual_currency_fx_rate"].fillna(df["currency_fx_rate"])
233
-
234
- df = df.where(pd.notnull(df), None).set_index("underlying_instrument")
235
- missing_prices = df.loc[df["price"].isnull(), "price"]
236
- if not missing_prices.empty:
237
- prices_df = pd.DataFrame(
238
- Instrument.objects.filter(id__in=missing_prices.index).dl.market_data(
239
- values=[MarketData.CLOSE], exact_date=sync_date
240
- )
241
- )
242
- if not prices_df.empty:
243
- prices_df = prices_df[["close", "instrument_id"]].set_index("instrument_id").astype("float")
244
- df.loc[prices_df.index, "price"] = prices_df
245
-
246
- if portfolio_total_value is not None:
247
- df["shares"] = (df["weighting"] * float(portfolio_total_value)) / (
248
- df["price"] * df["actual_currency_fx_rate"]
249
- )
250
- if is_estimated:
251
- df["is_estimated"] = True
252
- for underlying_instrument, asset_position in df.to_dict("index").items():
253
- if (
254
- asset_position["weighting"] or asset_position["shares"]
255
- ): # We don't yield empty position (pos with shares and weight equal to 0 or None)
256
- # We return the position as a serialized dictionary
257
- yield None, PositionDTO(
258
- date=sync_date,
259
- asset_valuation_date=sync_date,
260
- portfolio_created=portfolio_created.id,
261
- underlying_instrument=underlying_instrument,
262
- instrument_type=Instrument.objects.get(id=underlying_instrument).security_instrument_type,
263
- currency=asset_position["currency"],
264
- exchange=asset_position["exchange"],
265
- shares=asset_position["shares"],
266
- price=asset_position["price"],
267
- currency_fx_rate=asset_position["actual_currency_fx_rate"],
268
- weighting=asset_position["weighting"],
269
- is_estimated=asset_position["is_estimated"],
270
- )
271
-
272
- def __str__(self) -> str:
273
- return self.name
274
-
275
- @classmethod
276
- def get_representation_endpoint(cls) -> str:
277
- return "wbportfolio:portfoliosynchronizationrepresentation-list"
278
-
279
- @classmethod
280
- def get_representation_value_key(cls) -> str:
281
- return "id"
282
-
283
- @classmethod
284
- def get_representation_label_key(cls) -> str:
285
- return "{{name}}"
286
-
287
-
288
- @shared_task(queue="portfolio")
289
- def synchronize_portfolio_as_task(synchronization_method_id: int, portfolio_id: int, sync_date: date, **kwargs: Any):
290
- portfolio = Portfolio.objects.get(id=portfolio_id)
291
- synchronization_method = PortfolioSynchronization.objects.get(id=synchronization_method_id)
292
- synchronization_method.synchronize(portfolio, sync_date, **kwargs)