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
@@ -28,6 +28,7 @@ from wbcore.metadata.configs.buttons import ActionButton
28
28
  from wbcore.models import WBModel
29
29
  from wbcore.signals.models import pre_collection
30
30
  from wbfdm.models.instruments.instrument_prices import InstrumentPrice
31
+
31
32
  from wbportfolio.import_export.handlers.trade import TradeImportHandler
32
33
  from wbportfolio.models.asset import AssetPosition
33
34
  from wbportfolio.models.custodians import Custodian
@@ -42,8 +43,7 @@ class TradeQueryset(OrderedModelQuerySet):
42
43
  return self.annotate(
43
44
  last_effective_date=Subquery(
44
45
  AssetPosition.objects.filter(
45
- underlying_instrument=OuterRef("underlying_instrument"),
46
- date__lt=OuterRef("transaction_date"),
46
+ date__lte=OuterRef("value_date"),
47
47
  portfolio=OuterRef("portfolio"),
48
48
  )
49
49
  .order_by("-date")
@@ -52,7 +52,7 @@ class TradeQueryset(OrderedModelQuerySet):
52
52
  effective_weight=Coalesce(
53
53
  Subquery(
54
54
  AssetPosition.objects.filter(
55
- underlying_instrument=OuterRef("underlying_instrument"),
55
+ underlying_quote=OuterRef("underlying_instrument"),
56
56
  date=OuterRef("last_effective_date"),
57
57
  portfolio=OuterRef("portfolio"),
58
58
  )
@@ -66,7 +66,7 @@ class TradeQueryset(OrderedModelQuerySet):
66
66
  effective_shares=Coalesce(
67
67
  Subquery(
68
68
  AssetPosition.objects.filter(
69
- underlying_instrument=OuterRef("underlying_instrument"),
69
+ underlying_quote=OuterRef("underlying_instrument"),
70
70
  date=OuterRef("last_effective_date"),
71
71
  portfolio=OuterRef("portfolio"),
72
72
  )
@@ -76,7 +76,7 @@ class TradeQueryset(OrderedModelQuerySet):
76
76
  ),
77
77
  Decimal(0),
78
78
  ),
79
- target_shares=F("effective_weight") * F("weighting"),
79
+ target_shares=F("effective_shares") + F("shares"),
80
80
  )
81
81
 
82
82
 
@@ -222,7 +222,12 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
222
222
  @transition(
223
223
  field=status,
224
224
  source=Status.DRAFT,
225
- target=Status.SUBMIT,
225
+ target=GET_STATE(
226
+ lambda self, **kwargs: (
227
+ self.Status.SUBMIT if self.underlying_quote_price is not None else self.Status.FAILED
228
+ ),
229
+ states=[Status.SUBMIT, Status.FAILED],
230
+ ),
226
231
  permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
227
232
  user.profile, portfolio=instance.portfolio
228
233
  ),
@@ -237,40 +242,44 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
237
242
  # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
238
243
  )
239
244
  },
245
+ on_error="FAILED",
240
246
  )
241
247
  def submit(self, by=None, description=None, **kwargs):
242
- pass
248
+ if not self.underlying_quote_price:
249
+ self.comment = f"Trade failed because no price is found for {self.underlying_instrument.computed_str} on {self.transaction_date:%Y-%m-%d}"
243
250
 
244
251
  def can_submit(self):
245
252
  pass
246
253
 
247
254
  @transition(
248
255
  field=status,
249
- source=Status.SUBMIT,
256
+ source=Status.DRAFT,
250
257
  target=Status.FAILED,
251
258
  permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
252
259
  user.profile, portfolio=instance.portfolio
253
260
  ),
254
261
  )
255
262
  def fail(self, **kwargs):
256
- pass
263
+ self.comment = f"Trade failed because no price is found for {self.underlying_instrument.computed_str} on {self.transaction_date:%Y-%m-%d}"
257
264
 
258
265
  @cached_property
259
- def underlying_instrument_price(self) -> InstrumentPrice | None:
266
+ def underlying_quote_price(self) -> InstrumentPrice | None:
260
267
  try:
261
- return self.underlying_instrument.valuations.get(date=self.transaction_date)
268
+ return InstrumentPrice.objects.filter_only_valid_prices().get(
269
+ instrument=self.underlying_instrument, date=self.value_date
270
+ )
262
271
  except InstrumentPrice.DoesNotExist:
263
- return None
272
+ with suppress(InstrumentPrice.DoesNotExist):
273
+ return (
274
+ InstrumentPrice.objects.filter_only_valid_prices()
275
+ .filter(instrument=self.underlying_instrument, date__lte=self.value_date)
276
+ .latest("date")
277
+ )
264
278
 
265
279
  @transition(
266
280
  field=status,
267
281
  source=Status.SUBMIT,
268
- target=GET_STATE(
269
- lambda self, **kwargs: (
270
- self.Status.EXECUTED if self.underlying_instrument_price is not None else self.Status.FAILED
271
- ),
272
- states=[Status.EXECUTED, Status.FAILED],
273
- ),
282
+ target=Status.EXECUTED,
274
283
  permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
275
284
  user.profile, portfolio=instance.portfolio
276
285
  ),
@@ -285,30 +294,30 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
285
294
  # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
286
295
  )
287
296
  },
288
- on_error="FAILED",
289
297
  )
290
298
  def execute(self, **kwargs):
291
- if self.underlying_instrument_price:
299
+ if self.underlying_quote_price:
292
300
  asset, created = AssetPosition.unannotated_objects.update_or_create(
293
- underlying_instrument=self.underlying_instrument,
301
+ underlying_quote=self.underlying_instrument,
302
+ portfolio_created=None,
294
303
  portfolio=self.portfolio,
295
304
  date=self.transaction_date,
296
- is_estimated=False,
297
305
  defaults={
298
306
  "initial_currency_fx_rate": self.currency_fx_rate,
299
307
  "weighting": self._target_weight,
300
- "initial_price": self.underlying_instrument_price.net_value,
308
+ "initial_price": self.underlying_quote_price.net_value,
301
309
  "initial_shares": None,
302
- "underlying_instrument_price": self.underlying_instrument_price,
310
+ "underlying_quote_price": self.underlying_quote_price,
303
311
  "asset_valuation_date": self.transaction_date,
304
312
  "currency": self.currency,
313
+ "is_estimated": False,
305
314
  },
306
315
  )
307
316
  asset.set_weighting(self._target_weight)
308
- else:
309
- self.comment = f"Trade failed because no price is found for {self.underlying_instrument.computed_str} on {self.transaction_date:%Y-%m-%d}"
310
317
 
311
318
  def can_execute(self):
319
+ if not self.underlying_quote_price:
320
+ return {"underlying_instrument": "Cannot execute a trade without a valid quote price"}
312
321
  if not self.portfolio.is_manageable:
313
322
  return {"portfolio": "The portfolio needs to be a model portfolio in order to execute this trade manually"}
314
323
 
@@ -381,7 +390,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
381
390
  def revert(self, to_date=None, **kwargs):
382
391
  with suppress(AssetPosition.DoesNotExist):
383
392
  asset = AssetPosition.objects.get(
384
- underlying_instrument=self.underlying_instrument,
393
+ underlying_quote=self.underlying_instrument,
385
394
  portfolio=self.portfolio,
386
395
  date=self.transaction_date,
387
396
  is_estimated=False,
@@ -404,7 +413,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
404
413
  return self.last_effective_date
405
414
  elif (
406
415
  assets := AssetPosition.objects.filter(
407
- underlying_instrument=self.underlying_instrument,
416
+ underlying_quote=self.underlying_instrument,
408
417
  date__lt=self.transaction_date,
409
418
  portfolio=self.portfolio,
410
419
  )
@@ -418,7 +427,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
418
427
  self,
419
428
  "effective_weight",
420
429
  AssetPosition.objects.filter(
421
- underlying_instrument=self.underlying_instrument,
430
+ underlying_quote=self.underlying_instrument,
422
431
  date=self._last_effective_date,
423
432
  portfolio=self.portfolio,
424
433
  ).aggregate(s=Sum("weighting"))["s"]
@@ -432,7 +441,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
432
441
  self,
433
442
  "effective_shares",
434
443
  AssetPosition.objects.filter(
435
- underlying_instrument=self.underlying_instrument,
444
+ underlying_quote=self.underlying_instrument,
436
445
  date=self.transaction_date,
437
446
  portfolio=self.portfolio,
438
447
  ).aggregate(s=Sum("shares"))["s"]
@@ -447,7 +456,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
447
456
  @cached_property
448
457
  @admin.display(description="Target Shares")
449
458
  def _target_shares(self) -> Decimal:
450
- return getattr(self, "target_shares", self._effective_shares * self.weighting)
459
+ return getattr(self, "target_shares", self._effective_shares + self.shares)
451
460
 
452
461
  order_with_respect_to = "trade_proposal"
453
462
 
@@ -475,12 +484,19 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
475
484
  # notification_email_template = "portfolio/email/trade_notification.html"
476
485
 
477
486
  def save(self, *args, **kwargs):
487
+ if self.trade_proposal:
488
+ self.portfolio = self.trade_proposal.portfolio
489
+ self.transaction_date = self.trade_proposal.trade_date
490
+ self.value_date = self.trade_proposal.last_effective_date
491
+ if self._effective_shares:
492
+ self.shares = self._effective_shares * self.weighting
493
+
478
494
  if not self.custodian and self.bank:
479
495
  self.custodian = Custodian.get_by_mapping(self.bank)
480
496
  if self.price is None:
481
497
  # we try to get the price if not provided directly from the underlying instrument
482
498
  with suppress(InstrumentPrice.DoesNotExist):
483
- self.price = self.underlying_instrument.valuations.get(date=self.transaction_date).net_value
499
+ self.price = self.underlying_instrument.valuations.get(date=self.value_date).net_value
484
500
  if self.price is not None and self.price_gross is None:
485
501
  self.price_gross = self.price
486
502
 
@@ -489,12 +505,6 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
489
505
 
490
506
  if self.price_gross is not None and self.shares is not None and self.total_value_gross is None:
491
507
  self.total_value_gross = self.price_gross * self.shares
492
-
493
- if self.trade_proposal:
494
- self.portfolio = self.trade_proposal.portfolio
495
- self.transaction_date = self.trade_proposal.trade_date
496
- if effective_shares := self._effective_shares:
497
- self.shares = effective_shares * self.weighting
498
508
  self.transaction_type = Transaction.Type.TRADE
499
509
 
500
510
  if self.transaction_subtype is None:
@@ -516,8 +526,6 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
516
526
  self.claimed_shares = self.trade.claims.filter(status="APPROVED").aggregate(s=Sum("shares"))[
517
527
  "s"
518
528
  ] or Decimal(0)
519
- if self.trade_proposal and self.trade_proposal.status == "DRAFT":
520
- self.status = self.Status.DRAFT
521
529
  if self.internal_trade:
522
530
  self.marked_as_internal = True
523
531
  super().save(*args, **kwargs)
@@ -111,25 +111,19 @@ class Transaction(ImportMixin, models.Model):
111
111
  comment = models.TextField(default="", verbose_name="Comment", blank=True)
112
112
 
113
113
  def save(self, *args, **kwargs):
114
+ if not self.value_date:
115
+ self.value_date = self.transaction_date
116
+ if not self.book_date:
117
+ self.book_date = self.transaction_date
118
+
114
119
  if not getattr(self, "currency", None) and self.underlying_instrument:
115
120
  self.currency = self.underlying_instrument.currency
116
121
  if not self.currency_fx_rate:
117
122
  self.currency_fx_rate = self.underlying_instrument.currency.convert(
118
- self.transaction_date, self.portfolio.currency, exact_lookup=True
123
+ self.value_date, self.portfolio.currency, exact_lookup=True
119
124
  )
120
125
  if not self.transaction_type:
121
126
  self.transaction_type = self.__class__.__name__
122
- if not self.value_date:
123
- self.value_date = self.transaction_date
124
- # try:
125
- # # we try to find the next valid date (i.e. the one with position on the underlying instrument"
126
- # self.value_date = (
127
- # self.underlying_instrument.valuations.filter(date__gt=self.transaction_date).earliest("date").date
128
- # )
129
- # except ObjectDoesNotExist:
130
- # self.value_date = (self.transaction_date + BDay(1)).date()
131
- if not self.book_date:
132
- self.book_date = self.transaction_date
133
127
  if (
134
128
  self.total_value is not None
135
129
  and self.currency_fx_rate is not None
@@ -1,4 +1,5 @@
1
1
  from wbfdm.models import Instrument
2
+
2
3
  from wbportfolio.models import Index, Product
3
4
 
4
5
 
File without changes
@@ -0,0 +1,28 @@
1
+ import pandas as pd
2
+ from skfolio import Portfolio as BasePortfolio
3
+
4
+
5
+ class Portfolio(BasePortfolio):
6
+ def get_next_weights(self, returns: pd.Series) -> dict[int, float]:
7
+ """
8
+ Given the next returns, compute the next weights of this portfolio
9
+
10
+ Args:
11
+ returns: The returns for the next day as a pandas series
12
+
13
+ Returns:
14
+ A dictionary of weights (instrument ids as keys and weights as values)
15
+ """
16
+ weights = self.weights_per_observation.iloc[-1, :].T
17
+ if weights.sum() != 0:
18
+ weights /= weights.sum()
19
+ contribution = weights * (returns + 1.0)
20
+ if contribution.sum() != 0:
21
+ contribution /= contribution.sum()
22
+ return contribution.dropna().to_dict()
23
+
24
+ def get_estimate_net_value(self, previous_net_asset_value: float) -> float:
25
+ if self.previous_weights is None:
26
+ raise ValueError("No previous weights available")
27
+ expected_returns = self.previous_weights @ self.X.iloc[-1, :].T
28
+ return previous_net_asset_value * (1.0 + expected_returns)
@@ -2,6 +2,7 @@ from datetime import date
2
2
  from decimal import Decimal
3
3
 
4
4
  from django.core.exceptions import ValidationError
5
+
5
6
  from wbportfolio.pms.typing import Portfolio, Trade, TradeBatch
6
7
 
7
8
 
@@ -19,17 +20,15 @@ class TradingService:
19
20
  trades_batch: TradeBatch | None = None,
20
21
  total_value: Decimal = None,
21
22
  ):
22
- if not target_portfolio and not trades_batch:
23
- raise ValueError("Either target positions or trades needs to be provided")
24
23
  self.total_value = total_value
25
24
  self.trade_date = trade_date
25
+ if target_portfolio is None:
26
+ target_portfolio = Portfolio(positions=())
27
+ if effective_portfolio is None:
28
+ effective_portfolio = Portfolio(positions=())
26
29
  # If effective portfoolio and trades batch is provided, we ensure the trade batch contains at least one trade for every position
27
- if effective_portfolio and trades_batch:
28
- trades_batch = self.build_trade_batch(effective_portfolio, trades_batch=trades_batch)
30
+ trades_batch = self.build_trade_batch(effective_portfolio, target_portfolio, trades_batch=trades_batch)
29
31
  # if no trade but a effective portfolio is provided, we get the trade batch only from the effective portofolio (and the target portfolio if provided, but optional. Without it, the trade delta weight will be 0 )
30
- elif not trades_batch and effective_portfolio:
31
- # If no trade batch is provided but effetive_portfolio is, we estimate the trade from the given portfolios
32
- trades_batch = self.build_trade_batch(effective_portfolio, target_portfolio=target_portfolio)
33
32
  # Finally, we compute the target portfolio
34
33
  if trades_batch and not target_portfolio:
35
34
  target_portfolio = trades_batch.convert_to_portfolio()
@@ -75,7 +74,7 @@ class TradingService:
75
74
  def build_trade_batch(
76
75
  self,
77
76
  effective_portfolio: Portfolio,
78
- target_portfolio: Portfolio | None = None,
77
+ target_portfolio: Portfolio,
79
78
  trades_batch: TradeBatch | None = None,
80
79
  ) -> TradeBatch:
81
80
  """
@@ -89,25 +88,24 @@ class TradingService:
89
88
  Returns: The normalized trades batch
90
89
  """
91
90
  instruments = list(effective_portfolio.positions_map.keys())
92
- if target_portfolio:
93
- instruments.extend(list(target_portfolio.positions_map.keys()))
91
+ instruments.extend(list(target_portfolio.positions_map.keys()))
94
92
  if trades_batch:
95
93
  instruments.extend(list(trades_batch.trades_map.keys()))
96
94
  _trades: list[Trade] = []
97
95
  for instrument in set(instruments):
98
96
  effective_weight = target_weight = 0
99
- effective_shares = target_shares = 0
97
+ effective_shares = 0
100
98
  instrument_type = currency = None
101
99
  if effective_pos := effective_portfolio.positions_map.get(instrument, None):
102
100
  effective_weight = target_weight = effective_pos.weighting
103
- effective_shares = target_shares = effective_pos.shares
101
+ effective_shares = effective_pos.shares
104
102
  instrument_type, currency = effective_pos.instrument_type, effective_pos.currency
105
- if target_portfolio and (target_pos := target_portfolio.positions_map.get(instrument, None)):
103
+ if target_pos := target_portfolio.positions_map.get(instrument, None):
106
104
  target_weight = target_pos.weighting
107
- target_shares = target_pos.shares
105
+ instrument_type, currency = target_pos.instrument_type, target_pos.currency
108
106
  if trades_batch and (trade := trades_batch.trades_map.get(instrument, None)):
109
107
  effective_weight, target_weight = trade.effective_weight, trade.target_weight
110
- effective_shares, target_shares = trade.effective_shares, trade.target_shares
108
+ effective_shares = trade.effective_shares
111
109
  instrument_type, currency = trade.instrument_type, trade.currency
112
110
 
113
111
  _trades.append(
@@ -116,7 +114,6 @@ class TradingService:
116
114
  effective_weight=effective_weight,
117
115
  target_weight=target_weight,
118
116
  effective_shares=effective_shares,
119
- target_shares=target_shares,
120
117
  date=self.trade_date,
121
118
  instrument_type=instrument_type,
122
119
  currency=currency,
wbportfolio/pms/typing.py CHANGED
@@ -17,11 +17,11 @@ class Valuation:
17
17
  @dataclass(frozen=True)
18
18
  class Position:
19
19
  underlying_instrument: int
20
- instrument_type: int
21
20
  weighting: Decimal
22
- currency: int
23
21
  date: date_lib
24
22
 
23
+ currency: int | None = None
24
+ instrument_type: int | None = None
25
25
  asset_valuation_date: date_lib | None = None
26
26
  portfolio_created: int = None
27
27
  exchange: int = None
@@ -48,7 +48,7 @@ class Position:
48
48
 
49
49
  @dataclass(frozen=True)
50
50
  class Portfolio:
51
- positions: tuple[Position]
51
+ positions: tuple[Position] | tuple
52
52
  positions_map: dict[Position] = field(init=False, repr=False)
53
53
 
54
54
  def __post_init__(self):
@@ -62,7 +62,7 @@ class Portfolio:
62
62
 
63
63
  @cached_property
64
64
  def total_weight(self):
65
- return round(sum([pos.weighting for pos in self.positions]), 4)
65
+ return round(sum([pos.weighting for pos in self.positions]), 6)
66
66
 
67
67
  @cached_property
68
68
  def total_shares(self):
@@ -86,16 +86,12 @@ class Trade:
86
86
  target_weight: Decimal
87
87
  id: int | None = None
88
88
  effective_shares: Decimal = None
89
- target_shares: Decimal = None
90
89
 
91
90
  def __add__(self, other):
92
91
  return Trade(
93
92
  underlying_instrument=self.underlying_instrument,
94
93
  effective_weight=self.effective_weight + other.effective_weight,
95
94
  target_weight=self.target_weight + other.target_weight,
96
- target_shares=self.target_shares + other.target_shares
97
- if (self.target_shares is not None and other.target_shares is not None)
98
- else None,
99
95
  effective_shares=self.effective_shares + other.effective_shares
100
96
  if (self.effective_shares is not None and other.effective_shares is not None)
101
97
  else None,
@@ -106,7 +102,6 @@ class Trade:
106
102
  not in [
107
103
  "effective_weight",
108
104
  "target_weight",
109
- "target_shares",
110
105
  "effective_shares",
111
106
  "underlying_instrument",
112
107
  ]
@@ -118,22 +113,16 @@ class Trade:
118
113
  return self.target_weight - self.effective_weight
119
114
 
120
115
  def validate(self):
121
- if self.effective_weight < 0 or self.effective_weight > 1.0:
122
- raise ValidationError("Effective Weight needs to be in range [0, 1]")
123
- if self.target_weight < 0 or self.target_weight > 1.0:
124
- raise ValidationError("Target Weight needs to be in range [0, 1]")
116
+ return True
117
+ # if self.effective_weight < 0 or self.effective_weight > 1.0:
118
+ # raise ValidationError("Effective Weight needs to be in range [0, 1]")
119
+ # if self.target_weight < 0 or self.target_weight > 1.0:
120
+ # raise ValidationError("Target Weight needs to be in range [0, 1]")
125
121
 
126
122
  def normalize_target(self, total_target_weight: Decimal):
127
123
  t = Trade(
128
124
  target_weight=self.target_weight / total_target_weight if total_target_weight else self.target_weight,
129
- target_shares=self.target_shares / total_target_weight
130
- if (self.target_shares and total_target_weight)
131
- else self.target_shares,
132
- **{
133
- f.name: getattr(self, f.name)
134
- for f in fields(Trade)
135
- if f.name not in ["target_weight", "target_shares"]
136
- },
125
+ **{f.name: getattr(self, f.name) for f in fields(Trade) if f.name not in ["target_weight"]},
137
126
  )
138
127
  return t
139
128
 
@@ -154,15 +143,11 @@ class TradeBatch:
154
143
 
155
144
  @cached_property
156
145
  def total_target_weight(self) -> Decimal:
157
- return round(sum([trade.target_weight for trade in self.trades]), 4)
146
+ return round(sum([trade.target_weight for trade in self.trades]), 6)
158
147
 
159
148
  @cached_property
160
149
  def total_effective_weight(self) -> Decimal:
161
- return round(sum([trade.effective_weight for trade in self.trades]), 4)
162
-
163
- @cached_property
164
- def total_shares(self) -> Decimal:
165
- return sum([trade.target_shares for trade in self.trades if trade.target_shares is not None]) or Decimal(0)
150
+ return round(sum([trade.effective_weight for trade in self.trades]), 6)
166
151
 
167
152
  @cached_property
168
153
  def totat_abs_delta_weight(self) -> Decimal:
@@ -175,7 +160,7 @@ class TradeBatch:
175
160
  return len(self.trades)
176
161
 
177
162
  def validate(self):
178
- if float(self.total_target_weight) != 1.0: # we do that to remove decimal over precision
163
+ if round(float(self.total_target_weight), 4) != 1: # we do that to remove decimal over precision
179
164
  raise ValidationError(f"Total Weight cannot be different than 1 ({float(self.total_target_weight)})")
180
165
 
181
166
  def convert_to_portfolio(self):
@@ -186,7 +171,6 @@ class TradeBatch:
186
171
  underlying_instrument=trade.underlying_instrument,
187
172
  instrument_type=trade.instrument_type,
188
173
  weighting=trade.target_weight,
189
- shares=trade.target_shares,
190
174
  currency=trade.currency,
191
175
  date=trade.date,
192
176
  )
File without changes
@@ -0,0 +1,16 @@
1
+ from datetime import date
2
+
3
+ from wbportfolio.pms.typing import Portfolio as PortfolioDTO
4
+
5
+
6
+ class AbstractRebalancingModel:
7
+ def __init__(self, portfolio, trade_date: date, last_effective_date: date):
8
+ self.portfolio = portfolio
9
+ self.trade_date = trade_date
10
+ self.last_effective_date = last_effective_date
11
+
12
+ def is_valid(self) -> bool:
13
+ return True
14
+
15
+ def get_target_portfolio(self, **kwargs) -> PortfolioDTO:
16
+ raise NotImplementedError()
@@ -0,0 +1,17 @@
1
+ def register(model_name: str):
2
+ """
3
+ Decorator to include when a backend need automatic registration
4
+ """
5
+ from wbportfolio.models.transactions.rebalancing import RebalancingModel
6
+
7
+ def _decorator(backend_class):
8
+ defaults = {
9
+ "name": model_name,
10
+ }
11
+ RebalancingModel.objects.update_or_create(
12
+ class_path=backend_class.__module__ + "." + backend_class.__name__,
13
+ defaults=defaults,
14
+ )
15
+ return backend_class
16
+
17
+ return _decorator
@@ -0,0 +1,3 @@
1
+ from .composite import CompositeRebalancing
2
+ from .model_portfolio import ModelPortfolioRebalancing
3
+ from .equally_weighted import EquallyWeightedRebalancing
@@ -0,0 +1,31 @@
1
+ from decimal import Decimal
2
+
3
+ from django.core.exceptions import ObjectDoesNotExist
4
+
5
+ from wbportfolio.pms.typing import Portfolio, Position
6
+ from wbportfolio.rebalancing.base import AbstractRebalancingModel
7
+ from wbportfolio.rebalancing.decorators import register
8
+
9
+
10
+ @register("Composite Rebalancing")
11
+ class CompositeRebalancing(AbstractRebalancingModel):
12
+ @property
13
+ def base_assets(self) -> dict[int, Decimal]:
14
+ try:
15
+ latest_trade_proposal = self.portfolio.trade_proposals.filter(
16
+ status="APPROVED", trade_date__lte=self.trade_date
17
+ ).latest("trade_date")
18
+ return latest_trade_proposal.base_assets
19
+ except ObjectDoesNotExist:
20
+ return dict()
21
+
22
+ def is_valid(self) -> bool:
23
+ return len(self.base_assets.keys()) > 0
24
+
25
+ def get_target_portfolio(self, **kwargs) -> Portfolio:
26
+ positions = []
27
+ for underlying_instrument, weighting in self.base_assets.items():
28
+ positions.append(
29
+ Position(underlying_instrument=underlying_instrument, weighting=weighting, date=self.trade_date)
30
+ )
31
+ return Portfolio(positions=tuple(positions))
@@ -0,0 +1,21 @@
1
+ from decimal import Decimal
2
+
3
+ from wbportfolio.pms.typing import Portfolio
4
+ from wbportfolio.rebalancing.base import AbstractRebalancingModel
5
+ from wbportfolio.rebalancing.decorators import register
6
+
7
+
8
+ @register("Equally Weighted Rebalancing")
9
+ class EquallyWeightedRebalancing(AbstractRebalancingModel):
10
+ def is_valid(self) -> bool:
11
+ return self.portfolio.assets.filter(date=self.last_effective_date).exists()
12
+
13
+ def get_target_portfolio(self, **kwargs) -> Portfolio:
14
+ positions = []
15
+ assets = self.portfolio.assets.filter(date=self.last_effective_date)
16
+ nb_assets = assets.count()
17
+ for asset in assets:
18
+ asset.date = self.trade_date
19
+ asset.asset_valuation_date = self.trade_date
20
+ positions.append(asset._build_dto(new_weight=Decimal(1 / nb_assets)))
21
+ return Portfolio(positions=tuple(positions))
@@ -0,0 +1,35 @@
1
+ from wbportfolio.pms.typing import Portfolio
2
+ from wbportfolio.rebalancing.base import AbstractRebalancingModel
3
+ from wbportfolio.rebalancing.decorators import register
4
+
5
+
6
+ @register("Model Portfolio Rebalancing")
7
+ class ModelPortfolioRebalancing(AbstractRebalancingModel):
8
+ def __init__(self, *args, **kwargs):
9
+ super().__init__(*args, **kwargs)
10
+
11
+ @property
12
+ def model_portfolio_rel(self):
13
+ return self.portfolio.dependency_through.filter(type="MODEL").first()
14
+
15
+ @property
16
+ def model_portfolio(self):
17
+ if model_portfolio_rel := self.model_portfolio_rel:
18
+ return model_portfolio_rel.dependency_portfolio
19
+
20
+ def is_valid(self) -> bool:
21
+ return (
22
+ self.model_portfolio.assets.filter(date=self.last_effective_date).exists()
23
+ if self.model_portfolio
24
+ else False
25
+ )
26
+
27
+ def get_target_portfolio(self, **kwargs) -> Portfolio:
28
+ positions = []
29
+ assets = self.model_portfolio.get_positions(self.last_effective_date)
30
+
31
+ for asset in assets:
32
+ asset.date = self.trade_date
33
+ asset.asset_valuation_date = self.trade_date
34
+ positions.append(asset._build_dto())
35
+ return Portfolio(positions=tuple(positions))
@@ -54,7 +54,7 @@ class ReportClass(ReportMixin):
54
54
  "portfolio": instrument.name,
55
55
  "isin": position.underlying_instrument.isin,
56
56
  "title": position.underlying_instrument.name_repr,
57
- "instrument_type": position.underlying_instrument.security_instrument_type.short_name,
57
+ "instrument_type": position.underlying_instrument.instrument_type.short_name,
58
58
  "weight": float(position.weighting),
59
59
  "date": position.date.strftime("%Y-%m-%d"),
60
60
  }