wbportfolio 1.52.0__py2.py3-none-any.whl → 1.59.4__py2.py3-none-any.whl

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

Potentially problematic release.


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

Files changed (273) hide show
  1. wbportfolio/admin/__init__.py +3 -1
  2. wbportfolio/admin/indexes.py +1 -1
  3. wbportfolio/admin/orders/__init__.py +2 -0
  4. wbportfolio/admin/orders/order_proposals.py +16 -0
  5. wbportfolio/admin/orders/orders.py +32 -0
  6. wbportfolio/admin/portfolio.py +11 -5
  7. wbportfolio/admin/product_groups.py +1 -1
  8. wbportfolio/admin/products.py +2 -1
  9. wbportfolio/admin/{transactions/rebalancing.py → rebalancing.py} +1 -1
  10. wbportfolio/admin/transactions/__init__.py +0 -2
  11. wbportfolio/admin/transactions/dividends.py +40 -4
  12. wbportfolio/admin/transactions/fees.py +24 -14
  13. wbportfolio/admin/transactions/trades.py +34 -27
  14. wbportfolio/analysis/claims.py +5 -6
  15. wbportfolio/api_clients/ubs.py +162 -0
  16. wbportfolio/constants.py +1 -0
  17. wbportfolio/contrib/company_portfolio/configs/display.py +22 -10
  18. wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
  19. wbportfolio/contrib/company_portfolio/filters.py +10 -10
  20. wbportfolio/contrib/company_portfolio/models.py +69 -39
  21. wbportfolio/contrib/company_portfolio/scripts.py +7 -2
  22. wbportfolio/contrib/company_portfolio/serializers.py +32 -22
  23. wbportfolio/contrib/company_portfolio/tasks.py +12 -1
  24. wbportfolio/contrib/company_portfolio/tests/conftest.py +2 -2
  25. wbportfolio/defaults/fees/default.py +7 -15
  26. wbportfolio/factories/__init__.py +2 -2
  27. wbportfolio/factories/assets.py +1 -1
  28. wbportfolio/factories/dividends.py +8 -3
  29. wbportfolio/factories/fees.py +8 -4
  30. wbportfolio/factories/orders/__init__.py +2 -0
  31. wbportfolio/factories/orders/order_proposals.py +21 -0
  32. wbportfolio/factories/orders/orders.py +34 -0
  33. wbportfolio/factories/portfolios.py +2 -1
  34. wbportfolio/factories/product_groups.py +3 -3
  35. wbportfolio/factories/products.py +3 -3
  36. wbportfolio/factories/rebalancing.py +1 -1
  37. wbportfolio/factories/trades.py +12 -16
  38. wbportfolio/filters/assets.py +18 -4
  39. wbportfolio/filters/orders/__init__.py +2 -0
  40. wbportfolio/filters/orders/order_proposals.py +55 -0
  41. wbportfolio/filters/orders/orders.py +11 -0
  42. wbportfolio/filters/portfolios.py +38 -1
  43. wbportfolio/filters/positions.py +0 -1
  44. wbportfolio/filters/transactions/__init__.py +1 -2
  45. wbportfolio/filters/transactions/fees.py +5 -12
  46. wbportfolio/filters/transactions/trades.py +16 -8
  47. wbportfolio/filters/transactions/utils.py +42 -0
  48. wbportfolio/import_export/backends/ubs/__init__.py +1 -0
  49. wbportfolio/import_export/backends/ubs/asset_position.py +6 -7
  50. wbportfolio/import_export/backends/ubs/fees.py +10 -20
  51. wbportfolio/import_export/backends/ubs/instrument_price.py +6 -6
  52. wbportfolio/import_export/backends/ubs/trade.py +48 -0
  53. wbportfolio/import_export/backends/utils.py +0 -17
  54. wbportfolio/import_export/handlers/asset_position.py +22 -10
  55. wbportfolio/import_export/handlers/dividend.py +8 -8
  56. wbportfolio/import_export/handlers/fees.py +13 -23
  57. wbportfolio/import_export/handlers/orders.py +71 -0
  58. wbportfolio/import_export/handlers/trade.py +53 -77
  59. wbportfolio/import_export/parsers/default_mapping.py +1 -1
  60. wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +2 -2
  61. wbportfolio/import_export/parsers/jpmorgan/fees.py +4 -4
  62. wbportfolio/import_export/parsers/jpmorgan/strategy.py +59 -85
  63. wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
  64. wbportfolio/import_export/parsers/leonteq/customer_trade.py +5 -5
  65. wbportfolio/import_export/parsers/leonteq/fees.py +11 -7
  66. wbportfolio/import_export/parsers/leonteq/trade.py +2 -6
  67. wbportfolio/import_export/parsers/natixis/d1_fees.py +2 -2
  68. wbportfolio/import_export/parsers/natixis/dividend.py +4 -9
  69. wbportfolio/import_export/parsers/natixis/equity.py +22 -4
  70. wbportfolio/import_export/parsers/natixis/fees.py +7 -9
  71. wbportfolio/import_export/parsers/natixis/utils.py +13 -19
  72. wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +1 -1
  73. wbportfolio/import_export/parsers/sg_lux/equity.py +10 -10
  74. wbportfolio/import_export/parsers/sg_lux/fees.py +2 -2
  75. wbportfolio/import_export/parsers/sg_lux/perf_fees.py +2 -2
  76. wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
  77. wbportfolio/import_export/parsers/sg_lux/utils.py +2 -2
  78. wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
  79. wbportfolio/import_export/parsers/societe_generale/strategy.py +5 -5
  80. wbportfolio/import_export/parsers/tellco/customer_trade.py +2 -1
  81. wbportfolio/import_export/parsers/tellco/valuation.py +4 -3
  82. wbportfolio/import_export/parsers/ubs/api/fees.py +2 -2
  83. wbportfolio/import_export/parsers/ubs/api/trade.py +39 -0
  84. wbportfolio/import_export/parsers/ubs/customer_trade.py +7 -5
  85. wbportfolio/import_export/parsers/ubs/equity.py +3 -2
  86. wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
  87. wbportfolio/import_export/parsers/vontobel/customer_trade.py +2 -3
  88. wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +0 -1
  89. wbportfolio/import_export/parsers/vontobel/management_fees.py +12 -20
  90. wbportfolio/import_export/parsers/vontobel/performance_fees.py +5 -8
  91. wbportfolio/import_export/parsers/vontobel/valuation_api.py +4 -1
  92. wbportfolio/import_export/resources/trades.py +3 -3
  93. wbportfolio/import_export/utils.py +3 -1
  94. wbportfolio/jinja2/wbportfolio/sql/aum_nnm.sql +2 -2
  95. wbportfolio/metric/backends/base.py +2 -2
  96. wbportfolio/migrations/0059_fees_unique_fees.py +1 -1
  97. wbportfolio/migrations/0077_remove_transaction_currency_and_more.py +622 -0
  98. wbportfolio/migrations/0078_trade_drift_factor.py +26 -0
  99. wbportfolio/migrations/0079_alter_trade_drift_factor.py +19 -0
  100. wbportfolio/migrations/0080_alter_trade_drift_factor_alter_trade_weighting.py +19 -0
  101. wbportfolio/migrations/0081_alter_trade_drift_factor.py +19 -0
  102. wbportfolio/migrations/0082_remove_tradeproposal_creator_and_more.py +93 -0
  103. wbportfolio/migrations/0083_order_alter_trade_options_and_more.py +181 -0
  104. wbportfolio/migrations/0084_orderproposal_min_order_value.py +25 -0
  105. wbportfolio/migrations/0085_order_desired_target_weight.py +26 -0
  106. wbportfolio/migrations/0086_orderproposal_total_cash_weight.py +19 -0
  107. wbportfolio/migrations/0087_product_order_routing_custodian_adapter.py +94 -0
  108. wbportfolio/migrations/0088_orderproposal_total_effective_portfolio_contribution.py +19 -0
  109. wbportfolio/migrations/0089_orderproposal_min_weighting.py +71 -0
  110. wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
  111. wbportfolio/migrations/0091_remove_order_execution_confirmed_and_more.py +32 -0
  112. wbportfolio/migrations/0092_order_quantization_error_alter_orderproposal_status.py +49 -0
  113. wbportfolio/migrations/0093_remove_portfolioportfoliothroughmodel_unique_primary_and_more.py +35 -0
  114. wbportfolio/models/__init__.py +2 -0
  115. wbportfolio/models/adjustments.py +1 -1
  116. wbportfolio/models/asset.py +28 -170
  117. wbportfolio/models/builder.py +323 -0
  118. wbportfolio/models/custodians.py +3 -3
  119. wbportfolio/models/exceptions.py +1 -1
  120. wbportfolio/models/graphs/portfolio.py +1 -1
  121. wbportfolio/models/graphs/utils.py +11 -11
  122. wbportfolio/models/mixins/instruments.py +7 -0
  123. wbportfolio/models/mixins/liquidity_stress_test.py +4 -4
  124. wbportfolio/models/orders/__init__.py +2 -0
  125. wbportfolio/models/orders/order_proposals.py +1414 -0
  126. wbportfolio/models/orders/orders.py +410 -0
  127. wbportfolio/models/portfolio.py +311 -289
  128. wbportfolio/models/portfolio_relationship.py +6 -0
  129. wbportfolio/models/products.py +12 -0
  130. wbportfolio/models/{transactions/rebalancing.py → rebalancing.py} +40 -27
  131. wbportfolio/models/roles.py +4 -10
  132. wbportfolio/models/transactions/__init__.py +0 -4
  133. wbportfolio/models/transactions/claim.py +7 -6
  134. wbportfolio/models/transactions/dividends.py +42 -5
  135. wbportfolio/models/transactions/fees.py +55 -22
  136. wbportfolio/models/transactions/trades.py +121 -442
  137. wbportfolio/models/transactions/transactions.py +78 -158
  138. wbportfolio/models/utils.py +100 -1
  139. wbportfolio/order_routing/__init__.py +35 -0
  140. wbportfolio/order_routing/adapters/__init__.py +65 -0
  141. wbportfolio/order_routing/adapters/ubs.py +195 -0
  142. wbportfolio/order_routing/router.py +33 -0
  143. wbportfolio/order_routing/tests/__init__.py +0 -0
  144. wbportfolio/order_routing/tests/test_router.py +110 -0
  145. wbportfolio/permissions.py +7 -0
  146. wbportfolio/pms/analytics/portfolio.py +17 -9
  147. wbportfolio/pms/analytics/utils.py +9 -0
  148. wbportfolio/pms/trading/__init__.py +0 -1
  149. wbportfolio/pms/trading/optimizer.py +61 -0
  150. wbportfolio/pms/typing.py +198 -63
  151. wbportfolio/rebalancing/base.py +12 -1
  152. wbportfolio/rebalancing/decorators.py +1 -1
  153. wbportfolio/rebalancing/models/composite.py +4 -8
  154. wbportfolio/rebalancing/models/equally_weighted.py +13 -11
  155. wbportfolio/rebalancing/models/market_capitalization_weighted.py +21 -14
  156. wbportfolio/rebalancing/models/model_portfolio.py +14 -18
  157. wbportfolio/risk_management/backends/__init__.py +1 -0
  158. wbportfolio/risk_management/backends/controversy_portfolio.py +2 -2
  159. wbportfolio/risk_management/backends/esg_aggregation_portfolio.py +64 -0
  160. wbportfolio/risk_management/backends/exposure_portfolio.py +4 -4
  161. wbportfolio/risk_management/backends/instrument_list_portfolio.py +3 -3
  162. wbportfolio/risk_management/tests/test_esg_aggregation_portfolio.py +49 -0
  163. wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
  164. wbportfolio/risk_management/tests/test_stop_loss_instrument.py +2 -2
  165. wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -1
  166. wbportfolio/serializers/__init__.py +1 -0
  167. wbportfolio/serializers/orders/__init__.py +2 -0
  168. wbportfolio/serializers/orders/order_proposals.py +115 -0
  169. wbportfolio/serializers/orders/orders.py +283 -0
  170. wbportfolio/serializers/portfolios.py +7 -7
  171. wbportfolio/serializers/positions.py +2 -2
  172. wbportfolio/serializers/rebalancing.py +1 -1
  173. wbportfolio/serializers/signals.py +9 -12
  174. wbportfolio/serializers/transactions/__init__.py +1 -10
  175. wbportfolio/serializers/transactions/claim.py +2 -2
  176. wbportfolio/serializers/transactions/dividends.py +37 -9
  177. wbportfolio/serializers/transactions/fees.py +39 -10
  178. wbportfolio/serializers/transactions/trades.py +55 -157
  179. wbportfolio/tasks.py +43 -5
  180. wbportfolio/tests/analysis/__init__.py +0 -0
  181. wbportfolio/tests/analysis/test_claims.py +85 -0
  182. wbportfolio/tests/conftest.py +12 -12
  183. wbportfolio/tests/models/orders/__init__.py +0 -0
  184. wbportfolio/tests/models/orders/test_order_proposals.py +1046 -0
  185. wbportfolio/tests/models/test_assets.py +7 -3
  186. wbportfolio/tests/models/test_imports.py +9 -13
  187. wbportfolio/tests/models/test_portfolios.py +102 -95
  188. wbportfolio/tests/models/test_products.py +11 -0
  189. wbportfolio/tests/models/test_splits.py +1 -6
  190. wbportfolio/tests/models/test_utils.py +140 -0
  191. wbportfolio/tests/models/transactions/test_fees.py +7 -13
  192. wbportfolio/tests/models/transactions/test_rebalancing.py +5 -5
  193. wbportfolio/tests/models/transactions/test_trades.py +0 -20
  194. wbportfolio/tests/pms/test_analytics.py +22 -3
  195. wbportfolio/tests/rebalancing/test_models.py +51 -57
  196. wbportfolio/tests/signals.py +10 -20
  197. wbportfolio/tests/tests.py +3 -1
  198. wbportfolio/tests/viewsets/test_products.py +1 -0
  199. wbportfolio/urls.py +10 -13
  200. wbportfolio/viewsets/__init__.py +9 -4
  201. wbportfolio/viewsets/assets.py +3 -204
  202. wbportfolio/viewsets/charts/__init__.py +6 -1
  203. wbportfolio/viewsets/charts/assets.py +344 -154
  204. wbportfolio/viewsets/configs/buttons/__init__.py +2 -2
  205. wbportfolio/viewsets/configs/buttons/assets.py +1 -1
  206. wbportfolio/viewsets/configs/buttons/mixins.py +4 -4
  207. wbportfolio/viewsets/configs/buttons/portfolios.py +45 -1
  208. wbportfolio/viewsets/configs/buttons/products.py +32 -2
  209. wbportfolio/viewsets/configs/display/__init__.py +2 -5
  210. wbportfolio/viewsets/configs/display/assets.py +6 -19
  211. wbportfolio/viewsets/configs/display/fees.py +3 -3
  212. wbportfolio/viewsets/configs/display/portfolios.py +5 -5
  213. wbportfolio/viewsets/configs/display/products.py +1 -1
  214. wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
  215. wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
  216. wbportfolio/viewsets/configs/display/trades.py +1 -189
  217. wbportfolio/viewsets/configs/endpoints/__init__.py +3 -7
  218. wbportfolio/viewsets/configs/endpoints/fees.py +2 -2
  219. wbportfolio/viewsets/configs/endpoints/trades.py +0 -41
  220. wbportfolio/viewsets/configs/menu/__init__.py +1 -1
  221. wbportfolio/viewsets/configs/menu/orders.py +11 -0
  222. wbportfolio/viewsets/configs/titles/__init__.py +2 -3
  223. wbportfolio/viewsets/configs/titles/fees.py +4 -8
  224. wbportfolio/viewsets/esg.py +3 -5
  225. wbportfolio/viewsets/mixins.py +5 -1
  226. wbportfolio/viewsets/orders/__init__.py +6 -0
  227. wbportfolio/viewsets/orders/configs/__init__.py +4 -0
  228. wbportfolio/viewsets/orders/configs/buttons/__init__.py +2 -0
  229. wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +188 -0
  230. wbportfolio/viewsets/orders/configs/buttons/orders.py +113 -0
  231. wbportfolio/viewsets/orders/configs/displays/__init__.py +2 -0
  232. wbportfolio/viewsets/orders/configs/displays/order_proposals.py +157 -0
  233. wbportfolio/viewsets/orders/configs/displays/orders.py +232 -0
  234. wbportfolio/viewsets/orders/configs/endpoints/__init__.py +2 -0
  235. wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +21 -0
  236. wbportfolio/viewsets/orders/configs/endpoints/orders.py +28 -0
  237. wbportfolio/viewsets/orders/configs/titles/__init__.py +0 -0
  238. wbportfolio/viewsets/orders/configs/titles/orders.py +0 -0
  239. wbportfolio/viewsets/orders/order_proposals.py +252 -0
  240. wbportfolio/viewsets/orders/orders.py +277 -0
  241. wbportfolio/viewsets/portfolios.py +36 -12
  242. wbportfolio/viewsets/positions.py +3 -2
  243. wbportfolio/viewsets/products.py +6 -6
  244. wbportfolio/viewsets/{transactions/rebalancing.py → rebalancing.py} +2 -2
  245. wbportfolio/viewsets/transactions/__init__.py +3 -14
  246. wbportfolio/viewsets/transactions/fees.py +22 -22
  247. wbportfolio/viewsets/transactions/trades.py +1 -180
  248. {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/METADATA +3 -1
  249. {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/RECORD +252 -203
  250. {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/WHEEL +1 -1
  251. wbportfolio/admin/transactions/transactions.py +0 -38
  252. wbportfolio/factories/transactions.py +0 -22
  253. wbportfolio/fdm/tasks.py +0 -13
  254. wbportfolio/filters/transactions/transactions.py +0 -99
  255. wbportfolio/models/transactions/expiry.py +0 -7
  256. wbportfolio/models/transactions/trade_proposals.py +0 -704
  257. wbportfolio/pms/trading/handler.py +0 -161
  258. wbportfolio/serializers/transactions/expiry.py +0 -18
  259. wbportfolio/serializers/transactions/trade_proposals.py +0 -76
  260. wbportfolio/serializers/transactions/transactions.py +0 -85
  261. wbportfolio/tests/models/transactions/test_trade_proposals.py +0 -410
  262. wbportfolio/viewsets/configs/buttons/trade_proposals.py +0 -66
  263. wbportfolio/viewsets/configs/display/trade_proposals.py +0 -100
  264. wbportfolio/viewsets/configs/display/transactions.py +0 -55
  265. wbportfolio/viewsets/configs/endpoints/trade_proposals.py +0 -18
  266. wbportfolio/viewsets/configs/endpoints/transactions.py +0 -14
  267. wbportfolio/viewsets/configs/menu/transactions.py +0 -9
  268. wbportfolio/viewsets/configs/titles/transactions.py +0 -9
  269. wbportfolio/viewsets/signals.py +0 -43
  270. wbportfolio/viewsets/transactions/trade_proposals.py +0 -139
  271. wbportfolio/viewsets/transactions/transactions.py +0 -122
  272. /wbportfolio/{fdm → api_clients}/__init__.py +0 -0
  273. {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/licenses/LICENSE +0 -0
@@ -27,9 +27,13 @@ class TestAssetPositionModel:
27
27
  asset_position_factory.create(portfolio=portfolio, underlying_instrument=equity)
28
28
  assert AssetPosition.country_group_by(AssetPosition.objects.all()).values("groupby_id").distinct().count() == 1
29
29
 
30
- def test_exchange_group_by(self, asset_position_factory, portfolio, exchange):
31
- asset_position_factory.create(portfolio=portfolio, exchange=exchange)
32
- asset_position_factory.create(portfolio=portfolio, exchange=exchange)
30
+ def test_exchange_group_by(self, asset_position_factory, portfolio, exchange_factory, instrument):
31
+ asset_position_factory.create(
32
+ portfolio=portfolio, exchange=exchange_factory.create(), underlying_quote=instrument
33
+ )
34
+ asset_position_factory.create(
35
+ portfolio=portfolio, exchange=exchange_factory.create(), underlying_quote=instrument
36
+ )
33
37
  assert (
34
38
  AssetPosition.exchange_group_by(AssetPosition.objects.all()).values("groupby_id").distinct().count() == 1
35
39
  )
@@ -27,21 +27,19 @@ class TestImportMixinModel:
27
27
  data["currency"] = {"key": portfolio.currency.key}
28
28
  del data["id"]
29
29
  del data["import_source"]
30
- del data["transaction_ptr"]
31
30
  return data
32
31
 
33
32
  trade = trade_factory.build()
34
33
  data = {"data": [serialize(trade)]}
35
- handler = TradeImportHandler(import_source)
36
34
 
37
35
  # Import non existing data
38
- handler.process(data)
36
+ TradeImportHandler(import_source).process(data)
39
37
  assert Trade.objects.count() == 1
40
38
 
41
39
  # Import already existing data
42
40
  # import_source.data['data'][0]['shares'] *= 2
43
41
 
44
- handler.process(data)
42
+ TradeImportHandler(import_source).process(data)
45
43
  assert Trade.objects.count() == 1
46
44
 
47
45
  def test_import_price(self, import_source, product, instrument_price_factory, instrument):
@@ -76,16 +74,12 @@ class TestImportMixinModel:
76
74
 
77
75
  def serialize(fees):
78
76
  data = model_to_dict(fees)
79
- data["transaction_date"] = fees.transaction_date.strftime("%Y-%m-%d")
80
- data["value_date"] = fees.value_date.strftime("%Y-%m-%d")
81
- data["portfolio"] = portfolio.id
82
- data["linked_product"] = product.id
83
- data["portfolio"] = portfolio.id
77
+ data["fee_date"] = fees.fee_date.strftime("%Y-%m-%d")
78
+ data["product"] = product.id
84
79
  data["currency"] = {"key": portfolio.currency.key}
85
80
  del data["calculated"]
86
81
  del data["id"]
87
82
  del data["import_source"]
88
- del data["transaction_ptr"]
89
83
  return data
90
84
 
91
85
  fees = fees_factory.build(calculated=False)
@@ -142,7 +136,9 @@ class TestImportMixinModel:
142
136
  data = {
143
137
  "data": [
144
138
  self._serialize_position(
145
- asset_position_factory.build(date=val_date, underlying_instrument=instrument),
139
+ asset_position_factory.build(
140
+ date=val_date, underlying_instrument=instrument, weighting=Decimal("0.25")
141
+ ),
146
142
  product_portfolio,
147
143
  instrument,
148
144
  )
@@ -176,7 +172,7 @@ class TestImportMixinModel:
176
172
  def test_import_assetposition_product_group(
177
173
  self, import_source, product_group, currency, equity, asset_position_factory
178
174
  ):
179
- positions = asset_position_factory.build(underlying_instrument=equity)
175
+ positions = asset_position_factory.build(underlying_instrument=equity, weighting=Decimal("1"))
180
176
  data = {"data": [self._serialize_position(positions, product_group, equity)]}
181
177
 
182
178
  # Import non existing data
@@ -188,7 +184,7 @@ class TestImportMixinModel:
188
184
  def test_import_assetposition_index(
189
185
  self, import_source, index, portfolio, currency, equity, asset_position_factory
190
186
  ):
191
- positions = asset_position_factory.build(underlying_instrument=equity)
187
+ positions = asset_position_factory.build(underlying_instrument=equity, weighting=Decimal("1"))
192
188
  index.portfolios.add(portfolio)
193
189
  data = {"data": [self._serialize_position(positions, index, equity)]}
194
190
 
@@ -5,12 +5,12 @@ from unittest.mock import patch
5
5
 
6
6
  import pandas as pd
7
7
  import pytest
8
- from django.contrib.contenttypes.models import ContentType
9
8
  from django.db.models import F, Sum
10
9
  from django.forms.models import model_to_dict
11
10
  from faker import Faker
12
11
  from pandas.tseries.offsets import BDay
13
12
  from psycopg.types.range import DateRange
13
+ from wbcore.contrib.currency.factories import CurrencyFactory
14
14
  from wbcore.contrib.geography.factories import CountryFactory
15
15
 
16
16
  from wbportfolio.models import (
@@ -20,9 +20,8 @@ from wbportfolio.models import (
20
20
  PortfolioPortfolioThroughModel,
21
21
  Trade,
22
22
  )
23
- from wbportfolio.models.asset import AssetPositionIterator
24
23
 
25
- from ...models.portfolio import get_returns, update_portfolio_after_investable_universe
24
+ from ...models.portfolio import update_portfolio_after_investable_universe
26
25
  from .utils import PortfolioTestMixin
27
26
 
28
27
  fake = Faker()
@@ -108,11 +107,10 @@ class TestPortfolioModel(PortfolioTestMixin):
108
107
  )
109
108
  assert portfolio.get_geographical_breakdown(weekday).shape[0] == 2
110
109
 
111
- def test_get_currency_exposure(self, portfolio, asset_position_factory, currency_factory, equity_factory, weekday):
110
+ def test_get_currency_exposure(self, portfolio, asset_position_factory, equity_factory, weekday):
112
111
  a1 = asset_position_factory.create(
113
112
  portfolio=portfolio,
114
- underlying_instrument=equity_factory.create(),
115
- currency=currency_factory.create(),
113
+ underlying_instrument=equity_factory.create(currency=CurrencyFactory.create()),
116
114
  date=weekday,
117
115
  )
118
116
  asset_position_factory.create(
@@ -120,8 +118,7 @@ class TestPortfolioModel(PortfolioTestMixin):
120
118
  )
121
119
  asset_position_factory.create(
122
120
  portfolio=portfolio,
123
- underlying_instrument=equity_factory.create(),
124
- currency=currency_factory.create(),
121
+ underlying_instrument=equity_factory.create(currency=CurrencyFactory.create()),
125
122
  date=weekday,
126
123
  )
127
124
  assert portfolio.get_currency_exposure(weekday).shape[0] == 2
@@ -255,16 +252,18 @@ class TestPortfolioModel(PortfolioTestMixin):
255
252
 
256
253
  @patch.object(Portfolio, "estimate_net_asset_values", autospec=True)
257
254
  def test_change_at_date(self, mock_estimate_net_asset_values, asset_position_factory, portfolio, weekday):
258
- asset_position_factory.create_batch(10, portfolio=portfolio, date=weekday)
255
+ a1 = asset_position_factory.create(portfolio=portfolio, date=weekday, weighting=Decimal("0.1"))
256
+ a2 = asset_position_factory.create(portfolio=portfolio, date=weekday, weighting=Decimal("0.3"))
257
+ a3 = asset_position_factory.create(portfolio=portfolio, date=weekday, weighting=Decimal("0.5"))
259
258
 
260
- portfolio.change_at_date(weekday, recompute_weighting=True)
259
+ portfolio.change_at_date(weekday, fix_quantization=True)
261
260
 
262
- # test that change at date normalize the weighting
263
- total_value = AssetPosition.objects.aggregate(s=Sum("total_value_fx_portfolio"))["s"]
264
- for pos in AssetPosition.objects.all():
265
- assert float(pos.weighting) == pytest.approx(float(pos.total_value_fx_portfolio / total_value), rel=1e-2)
261
+ cash_pos = portfolio.assets.get(date=weekday, portfolio=portfolio, underlying_quote=portfolio.cash_component)
262
+ assert cash_pos.weighting == Decimal("1.0") - (a1.weighting + a2.weighting + a3.weighting)
266
263
 
267
- mock_estimate_net_asset_values.assert_called_once_with(portfolio, (weekday + BDay(1)).date(), weights=None)
264
+ mock_estimate_net_asset_values.assert_called_once_with(
265
+ portfolio, (weekday + BDay(1)).date(), analytic_portfolio=None
266
+ )
268
267
 
269
268
  @patch.object(Portfolio, "compute_lookthrough", autospec=True)
270
269
  def test_change_at_date_with_dependent_portfolio(
@@ -282,7 +281,10 @@ class TestPortfolioModel(PortfolioTestMixin):
282
281
  dependent_portfolio.depends_on.add(base_portfolio)
283
282
  base_portfolio.change_at_date(weekday)
284
283
 
285
- mock_compute_lookthrough.assert_called_once_with(dependent_portfolio, weekday)
284
+ mock_compute_lookthrough.assert_called_once_with(
285
+ dependent_portfolio,
286
+ weekday,
287
+ )
286
288
 
287
289
  def test_is_active_at_date(
288
290
  self,
@@ -527,8 +529,9 @@ class TestPortfolioModel(PortfolioTestMixin):
527
529
  assert res1
528
530
 
529
531
  def test_get_total_asset_under_management(
530
- self, portfolio, customer_trade_factory, instrument_factory, instrument_price_factory, weekday
532
+ self, portfolio_factory, customer_trade_factory, instrument_factory, instrument_price_factory, weekday
531
533
  ):
534
+ portfolio = portfolio_factory.create()
532
535
  i1 = instrument_factory.create()
533
536
  i2 = instrument_factory.create()
534
537
  previous_day = (weekday - BDay(5)).date()
@@ -921,13 +924,13 @@ class TestPortfolioModel(PortfolioTestMixin):
921
924
 
922
925
  analytic_portfolio = portfolio.get_analytic_portfolio(weekday)
923
926
  assert analytic_portfolio.weights.tolist() == [float(a1.weighting), float(a2.weighting)]
924
- expected_X = pd.DataFrame(
927
+ expected_x = pd.DataFrame(
925
928
  [[float(p11.net_value / p10.net_value - Decimal(1)), float(p21.net_value / p20.net_value - Decimal(1))]],
926
929
  columns=[i1.id, i2.id],
927
930
  index=[(weekday + BDay(1)).date()],
928
931
  )
929
- expected_X.index = pd.to_datetime(expected_X.index)
930
- pd.testing.assert_frame_equal(analytic_portfolio.X, expected_X, check_names=False, check_freq=False)
932
+ expected_x.index = pd.to_datetime(expected_x.index)
933
+ pd.testing.assert_frame_equal(analytic_portfolio.X, expected_x, check_names=False, check_freq=False)
931
934
 
932
935
  def test_get_total_asset_value(self, weekday, portfolio, asset_position_factory):
933
936
  a1 = asset_position_factory.create(date=weekday, portfolio=portfolio)
@@ -998,20 +1001,18 @@ class TestPortfolioModel(PortfolioTestMixin):
998
1001
  fx_portfolio = currency_fx_rates_factory.create(currency=portfolio.currency, date=weekday)
999
1002
  fx_instrument = currency_fx_rates_factory.create(currency=instrument.currency, date=weekday)
1000
1003
  instrument_id: int = instrument.id
1001
- weights = {instrument_id: random.random()}
1002
- positions = AssetPositionIterator(
1003
- portfolio, prices={weekday: {instrument_id: p.net_value}}, infer_underlying_quote_price=False
1004
- )
1005
- positions.add((weekday, weights))
1004
+ weights = {instrument_id: Decimal(random.random())}
1005
+ portfolio.builder.prices = {weekday: {instrument_id: p.net_value}}
1006
+ portfolio.builder.add((weekday, weights), infer_underlying_quote_price=False)
1006
1007
 
1007
- res = list(positions)
1008
+ res = list(portfolio.builder.get_positions())
1008
1009
  a = res[0]
1009
1010
  assert len(res) == 1
1010
1011
  assert a.date == weekday
1011
1012
  assert a.underlying_quote == instrument
1012
- assert a.underlying_quote_price == None
1013
+ assert a.underlying_quote_price is None
1013
1014
  assert a.initial_price == p.net_value
1014
- assert a.weighting == weights[instrument.id]
1015
+ assert a.weighting == pytest.approx(weights[instrument.id], abs=Decimal(10e-6))
1015
1016
  assert a.currency_fx_rate_portfolio_to_usd == fx_portfolio
1016
1017
  assert a.currency_fx_rate_instrument_to_usd == fx_instrument
1017
1018
 
@@ -1033,45 +1034,48 @@ class TestPortfolioModel(PortfolioTestMixin):
1033
1034
 
1034
1035
  i1 = instrument_factory.create(currency=portfolio.currency)
1035
1036
  i2 = instrument_factory.create(currency=portfolio.currency)
1037
+
1038
+ instrument_price_factory.create(net_value=Decimal("100"), instrument=i1, date=(weekday - BDay(1)).date())
1039
+ instrument_price_factory.create(net_value=Decimal("100"), instrument=i1, date=weekday)
1040
+ instrument_price_factory.create(net_value=Decimal("100"), instrument=i1, date=middle_date)
1041
+ instrument_price_factory.create(net_value=Decimal("100"), instrument=i1, date=rebalancing_date)
1042
+ instrument_price_factory.create(net_value=Decimal("100"), instrument=i2, date=(weekday - BDay(1)).date())
1043
+ instrument_price_factory.create(net_value=Decimal("100"), instrument=i2, date=weekday)
1044
+ instrument_price_factory.create(net_value=Decimal("100"), instrument=i2, date=middle_date)
1045
+ instrument_price_factory.create(net_value=Decimal("100"), instrument=i2, date=rebalancing_date)
1046
+
1036
1047
  asset_position_factory.create(date=weekday, portfolio=portfolio, underlying_instrument=i1, weighting=0.7)
1037
1048
  asset_position_factory.create(date=weekday, portfolio=portfolio, underlying_instrument=i2, weighting=0.3)
1038
1049
 
1039
- instrument_price_factory.create(instrument=i1, date=(weekday - BDay(1)).date())
1040
- instrument_price_factory.create(instrument=i1, date=weekday)
1041
- instrument_price_factory.create(instrument=i1, date=middle_date)
1042
- instrument_price_factory.create(instrument=i1, date=rebalancing_date)
1043
- instrument_price_factory.create(instrument=i2, date=(weekday - BDay(1)).date())
1044
- instrument_price_factory.create(instrument=i2, date=weekday)
1045
- instrument_price_factory.create(instrument=i2, date=middle_date)
1046
- instrument_price_factory.create(instrument=i2, date=rebalancing_date)
1047
-
1048
1050
  rebalancer_factory.create(portfolio=portfolio, frequency="RRULE:FREQ=DAILY;", activation_date=rebalancing_date)
1049
- positions, rebalancing_trade_proposal = portfolio.drift_weights(weekday, (rebalancing_date + BDay(1)).date())
1050
- assert rebalancing_trade_proposal.trade_date == rebalancing_date
1051
- assert rebalancing_trade_proposal.status == "SUBMIT"
1052
- assert set(positions.get_weights().keys()) == {
1053
- middle_date,
1054
- }, "Drifting weight with a non automatic rebalancer stops the iteration"
1055
-
1056
- # we expect a equally rebalancing (default) so both trades needs to be created
1057
- t1 = rebalancing_trade_proposal.trades.get(transaction_date=rebalancing_date, underlying_instrument=i1)
1058
- t2 = rebalancing_trade_proposal.trades.get(transaction_date=rebalancing_date, underlying_instrument=i2)
1059
- assert t1._target_weight == Decimal("0.5")
1060
- assert t2._target_weight == Decimal("0.5")
1061
-
1062
- # we approve the rebalancing trade proposal
1063
- assert rebalancing_trade_proposal.status == "SUBMIT"
1064
- rebalancing_trade_proposal.approve(no_replay=True)
1065
- rebalancing_trade_proposal.save()
1051
+ portfolio.load_builder_returns(weekday, rebalancing_date)
1052
+ gen = portfolio.drift_weights(weekday, (rebalancing_date + BDay(1)).date(), stop_at_rebalancing=True)
1053
+ assert next(gen)[0] == middle_date, "Drifting weight with a non automatic rebalancer stops the iteration"
1054
+ try:
1055
+ next(gen)
1056
+ raise AssertionError("the next iteration should stop and return the rebalancing")
1057
+ except StopIteration as e:
1058
+ rebalancing_order_proposal = e.value
1059
+ assert rebalancing_order_proposal.trade_date == rebalancing_date
1060
+ assert rebalancing_order_proposal.status == "PENDING"
1061
+
1062
+ # we expect a equally rebalancing (default) so both orders needs to be created
1063
+ orders = rebalancing_order_proposal.get_orders()
1064
+ t1 = orders.get(value_date=rebalancing_date, underlying_instrument=i1)
1065
+ t2 = orders.get(value_date=rebalancing_date, underlying_instrument=i2)
1066
+ assert t1._target_weight == Decimal("0.5")
1067
+ assert t2._target_weight == Decimal("0.5")
1068
+
1069
+ rebalancing_order_proposal.approve()
1070
+ rebalancing_order_proposal.save()
1071
+ # we approve the rebalancing order proposal
1072
+ assert rebalancing_order_proposal.status == "APPROVED"
1066
1073
 
1067
1074
  # check that the rebalancing was applied and position reflect that
1068
1075
  assert portfolio.assets.get(date=rebalancing_date, underlying_instrument=i1).weighting == Decimal("0.5")
1069
1076
  assert portfolio.assets.get(date=rebalancing_date, underlying_instrument=i2).weighting == Decimal("0.5")
1070
1077
 
1071
- @patch("wbportfolio.models.portfolio.compute_metrics_as_task.delay")
1072
- def test_bulk_create_positions(
1073
- self, mock_compute_metrics, portfolio, weekday, asset_position_factory, instrument_factory
1074
- ):
1078
+ def test_bulk_create_positions(self, portfolio, weekday, asset_position_factory, instrument_factory):
1075
1079
  portfolio.is_manageable = False
1076
1080
  portfolio.save()
1077
1081
  i1 = instrument_factory.create()
@@ -1080,21 +1084,17 @@ class TestPortfolioModel(PortfolioTestMixin):
1080
1084
  a1 = asset_position_factory.build(date=weekday, portfolio=portfolio, underlying_instrument=i1)
1081
1085
 
1082
1086
  # check initial creation
1083
- portfolio.bulk_create_positions(AssetPositionIterator(portfolio).add([a1]))
1087
+ portfolio.builder.add([a1]).bulk_create_positions(fix_quantization=False, compute_metrics=True)
1084
1088
  assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).weighting == a1.weighting
1085
1089
  assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).underlying_instrument == i1
1086
1090
 
1087
- mock_compute_metrics.assert_called_once_with(
1088
- weekday, basket_id=portfolio.id, basket_content_type_id=ContentType.objects.get_for_model(Portfolio).id
1089
- )
1090
-
1091
- # check that if we change key value, an already exising position will be updated accordingly
1091
+ # check that if we change key value, an already existing position will be updated accordingly
1092
1092
  a1.weighting = Decimal(0.5)
1093
- portfolio.bulk_create_positions(AssetPositionIterator(portfolio).add([a1]))
1093
+ portfolio.builder.add([a1]).bulk_create_positions(fix_quantization=False)
1094
1094
  assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).weighting == Decimal(0.5)
1095
1095
 
1096
1096
  a2 = asset_position_factory.build(date=weekday, portfolio=portfolio, underlying_instrument=i2)
1097
- portfolio.bulk_create_positions(AssetPositionIterator(portfolio).add([a2]))
1097
+ portfolio.builder.add([a2]).bulk_create_positions(fix_quantization=False)
1098
1098
  assert (
1099
1099
  AssetPosition.objects.get(portfolio=portfolio, date=weekday, underlying_instrument=i1).weighting
1100
1100
  == a1.weighting
@@ -1105,7 +1105,7 @@ class TestPortfolioModel(PortfolioTestMixin):
1105
1105
  )
1106
1106
 
1107
1107
  a3 = asset_position_factory.build(date=weekday, portfolio=portfolio, underlying_instrument=i3)
1108
- portfolio.bulk_create_positions(AssetPositionIterator(portfolio).add([a3]), delete_leftovers=True)
1108
+ portfolio.builder.add([a3]).bulk_create_positions(delete_leftovers=True, fix_quantization=False)
1109
1109
  assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).weighting == a3.weighting
1110
1110
  assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).underlying_instrument == i3
1111
1111
 
@@ -1124,33 +1124,6 @@ class TestPortfolioModel(PortfolioTestMixin):
1124
1124
  res = list(Portfolio.objects.all().to_dependency_iterator(weekday))
1125
1125
  assert res == [index_portfolio, dependency_portfolio, dependant_portfolio, undependant_portfolio]
1126
1126
 
1127
- def test_get_returns(self, instrument_factory, instrument_price_factory, asset_position_factory, portfolio):
1128
- v1 = date(2025, 1, 1)
1129
- v2 = date(2025, 1, 2)
1130
- v3 = date(2025, 1, 3)
1131
-
1132
- i1 = instrument_factory.create()
1133
- i2 = instrument_factory.create()
1134
-
1135
- i11 = instrument_price_factory.create(date=v1, instrument=i1)
1136
- i12 = instrument_price_factory.create(date=v2, instrument=i1)
1137
- i13 = instrument_price_factory.create(date=v3, instrument=i1)
1138
- asset_position_factory.create(date=v1, portfolio=portfolio, underlying_instrument=i1)
1139
- asset_position_factory.create(date=v3, portfolio=portfolio, underlying_instrument=i2)
1140
- i11.refresh_from_db()
1141
- i12.refresh_from_db()
1142
- i13.refresh_from_db()
1143
- returns = get_returns([i1.id, i2.id], from_date=v1, to_date=v3)
1144
-
1145
- expected_returns = pd.DataFrame(
1146
- [[i12.net_value / i11.net_value - 1, 0.0], [i13.net_value / i12.net_value - 1, 0.0]],
1147
- index=[v2, v3],
1148
- columns=[i1.id, i2.id],
1149
- dtype="float64",
1150
- )
1151
- expected_returns.index = pd.to_datetime(expected_returns.index)
1152
- pd.testing.assert_frame_equal(returns, expected_returns, check_names=False, check_freq=False, atol=1e-6)
1153
-
1154
1127
  @patch.object(Portfolio, "compute_lookthrough", autospec=True)
1155
1128
  def test_handle_controlling_portfolio_change_at_date(self, mock_compute_lookthrough, weekday, portfolio_factory):
1156
1129
  primary_portfolio = portfolio_factory.create(only_weighting=True)
@@ -1158,8 +1131,42 @@ class TestPortfolioModel(PortfolioTestMixin):
1158
1131
  PortfolioPortfolioThroughModel.objects.create(
1159
1132
  portfolio=lookthrough_portfolio,
1160
1133
  dependency_portfolio=primary_portfolio,
1161
- type=PortfolioPortfolioThroughModel.Type.PRIMARY,
1134
+ type=PortfolioPortfolioThroughModel.Type.LOOK_THROUGH,
1162
1135
  )
1163
1136
 
1164
1137
  primary_portfolio.handle_controlling_portfolio_change_at_date(weekday)
1165
1138
  mock_compute_lookthrough.assert_called_once_with(lookthrough_portfolio, weekday)
1139
+
1140
+ def test_get_model_portfolio_relationships(self, portfolio_factory, asset_position_factory, weekday):
1141
+ model_portfolio = portfolio_factory.create()
1142
+ model_index = model_portfolio.get_or_create_index()
1143
+ dependent_portfolio = portfolio_factory.create()
1144
+ re1 = PortfolioPortfolioThroughModel.objects.create(
1145
+ portfolio=dependent_portfolio,
1146
+ dependency_portfolio=model_portfolio,
1147
+ type=PortfolioPortfolioThroughModel.Type.MODEL,
1148
+ )
1149
+
1150
+ assert set(model_portfolio.get_model_portfolio_relationships(weekday)) == {
1151
+ re1,
1152
+ }
1153
+ parent_portfolio = portfolio_factory.create()
1154
+ child_portfolio = portfolio_factory.create()
1155
+ re2 = PortfolioPortfolioThroughModel.objects.create(
1156
+ portfolio=child_portfolio,
1157
+ dependency_portfolio=parent_portfolio,
1158
+ type=PortfolioPortfolioThroughModel.Type.MODEL,
1159
+ )
1160
+ assert set(model_portfolio.get_model_portfolio_relationships(weekday)) == {
1161
+ re1,
1162
+ } # child portfolio is not considered in the tree because there is no position yet
1163
+
1164
+ asset_position_factory.create(portfolio=parent_portfolio, underlying_instrument=model_index, date=weekday)
1165
+ assert set(model_portfolio.get_model_portfolio_relationships(weekday)) == {re1, re2}
1166
+
1167
+ dependent_portfolio.is_active = False # disable this portfolio
1168
+ dependent_portfolio.deletion_datetime = weekday - timedelta(days=1)
1169
+ dependent_portfolio.save()
1170
+ assert set(model_portfolio.get_model_portfolio_relationships(weekday)) == {
1171
+ re2,
1172
+ }
@@ -215,3 +215,14 @@ class TestProductModel(PortfolioTestMixin):
215
215
  body=f"The product {product} will be terminated on the {product.delisted_date:%Y-%m-%d}",
216
216
  user=internal_user,
217
217
  )
218
+
219
+ def test_delist_product_disable_report(self, product, report_factory):
220
+ report = report_factory.create(content_object=product, is_active=True)
221
+ assert product.delisted_date is None
222
+ assert report.is_active
223
+
224
+ product.delisted_date = datetime.date.today()
225
+ product.save()
226
+
227
+ report.refresh_from_db()
228
+ assert report.is_active is False
@@ -14,10 +14,6 @@ fake = Faker()
14
14
 
15
15
  @pytest.mark.django_db
16
16
  class TestAdjustmentModel:
17
- @pytest.fixture()
18
- def applied_adjustment(self):
19
- return AdjustmentFactory.create(status=Adjustment.Status.APPLIED)
20
-
21
17
  @pytest.fixture()
22
18
  def old_adjustment(self):
23
19
  return AdjustmentFactory.create(status=Adjustment.Status.PENDING, date=fake.past_date())
@@ -205,7 +201,7 @@ class TestAdjustmentModel:
205
201
  post_adjustment_on_prices(adjustment.id)
206
202
  a1.refresh_from_db()
207
203
  adjustment.refresh_from_db()
208
- a1.applied_adjustment == adjustment
204
+ assert a1.applied_adjustment == adjustment
209
205
  assert adjustment.status == Adjustment.Status.APPLIED
210
206
 
211
207
  @patch("wbportfolio.models.adjustments.send_notification")
@@ -230,5 +226,4 @@ class TestAdjustmentModel:
230
226
  mock_check_fct.return_value = False
231
227
  post_adjustment_on_prices(adjustment.id)
232
228
  adjustment.refresh_from_db()
233
- mock_delay_fct.call_args[0] == user_porftolio_manager.id
234
229
  assert adjustment.status == Adjustment.Status.PENDING
@@ -0,0 +1,140 @@
1
+ from decimal import Decimal
2
+ from unittest.mock import patch
3
+
4
+ import pytest
5
+ from pandas._libs.tslibs.offsets import BDay
6
+
7
+ from wbportfolio.models import AssetPosition, Order, OrderProposal, Portfolio
8
+ from wbportfolio.models.utils import adjust_assets, adjust_orders, adjust_quote, get_adjusted_shares
9
+
10
+
11
+ def test_get_adjusted_shares():
12
+ assert get_adjusted_shares(Decimal("150"), Decimal("100"), Decimal("200")) == Decimal("75")
13
+
14
+
15
+ @pytest.mark.django_db
16
+ @patch.object(Order, "_get_price")
17
+ def test_adjust_orders(mock_get_price, weekday, order_factory, instrument_factory):
18
+ mock_get_price.return_value = (Decimal("100"), Decimal("0"))
19
+ old_quote = instrument_factory.create()
20
+ new_quote = instrument_factory.create()
21
+ o1 = order_factory.create(
22
+ underlying_instrument=old_quote, order_proposal__trade_date=weekday, price=Decimal("100"), shares=Decimal("10")
23
+ )
24
+ o2 = order_factory.create(
25
+ underlying_instrument=old_quote, order_proposal__trade_date=weekday, price=Decimal("100"), shares=Decimal("20")
26
+ )
27
+ adjust_orders(Order.objects.filter(underlying_instrument=old_quote), new_quote)
28
+
29
+ o1.refresh_from_db()
30
+ o2.refresh_from_db()
31
+
32
+ assert o1.underlying_instrument == new_quote
33
+ assert o2.underlying_instrument == new_quote
34
+ assert o1.price == Decimal("100")
35
+ assert o2.price == Decimal("100")
36
+ assert o1.shares == Decimal("10")
37
+ assert o2.shares == Decimal("20")
38
+
39
+ mock_get_price.return_value = (Decimal("200"), Decimal("0"))
40
+ adjust_orders(Order.objects.filter(underlying_instrument=new_quote), old_quote)
41
+
42
+ o1.refresh_from_db()
43
+ o2.refresh_from_db()
44
+
45
+ assert o1.underlying_instrument == old_quote
46
+ assert o2.underlying_instrument == old_quote
47
+ assert o1.price == Decimal("200")
48
+ assert o2.price == Decimal("200")
49
+ assert o1.shares == Decimal("5")
50
+ assert o2.shares == Decimal("10")
51
+
52
+
53
+ @pytest.mark.django_db
54
+ def test_adjust_assets(weekday, asset_position_factory, instrument_factory, instrument_price_factory):
55
+ old_quote = instrument_factory.create()
56
+ new_quote = instrument_factory.create()
57
+ old_quote_price = instrument_price_factory.create(
58
+ instrument=old_quote, net_value=Decimal("100"), date=weekday, calculated=False
59
+ )
60
+ new_quote_price = instrument_price_factory.create(
61
+ instrument=new_quote, net_value=Decimal("100"), date=weekday, calculated=False
62
+ )
63
+
64
+ a1 = asset_position_factory.create(
65
+ underlying_quote=old_quote, date=weekday, initial_price=Decimal("100"), initial_shares=Decimal("10")
66
+ )
67
+ a2 = asset_position_factory.create(
68
+ underlying_quote=old_quote, date=weekday, initial_price=Decimal("100"), initial_shares=Decimal("20")
69
+ )
70
+ adjust_assets(AssetPosition.objects.filter(underlying_quote=old_quote), new_quote)
71
+
72
+ a1.refresh_from_db()
73
+ a2.refresh_from_db()
74
+
75
+ assert a1.underlying_quote == new_quote
76
+ assert a2.underlying_quote == new_quote
77
+ assert a1.underlying_quote_price == new_quote_price
78
+ assert a2.underlying_quote_price == new_quote_price
79
+ assert a1.initial_price == Decimal("100")
80
+ assert a2.initial_price == Decimal("100")
81
+ assert a1.initial_shares == Decimal("10")
82
+ assert a2.initial_shares == Decimal("20")
83
+
84
+ old_quote_price.net_value = new_quote_price.net_value = Decimal("200")
85
+ old_quote_price.save()
86
+ new_quote_price.save()
87
+
88
+ adjust_assets(AssetPosition.objects.filter(underlying_quote=new_quote), old_quote)
89
+
90
+ a1.refresh_from_db()
91
+ a2.refresh_from_db()
92
+
93
+ assert a1.underlying_quote == old_quote
94
+ assert a2.underlying_quote == old_quote
95
+ assert a1.underlying_quote_price == old_quote_price
96
+ assert a2.underlying_quote_price == old_quote_price
97
+ assert a1.initial_price == Decimal("200")
98
+ assert a2.initial_price == Decimal("200")
99
+ assert a1.initial_shares == Decimal("5")
100
+ assert a2.initial_shares == Decimal("10")
101
+
102
+
103
+ @pytest.mark.django_db
104
+ @patch("wbportfolio.models.utils.adjust_assets")
105
+ @patch("wbportfolio.models.utils.adjust_orders")
106
+ @patch.object(OrderProposal, "replay")
107
+ def test_adjust_quote(mock_replay, mock_adjust_orders, mock_adjust_assets, weekday, order_factory, instrument_factory):
108
+ old_quote = instrument_factory.create()
109
+ new_quote = instrument_factory.create()
110
+ o1 = order_factory.create( # noqa: F841
111
+ underlying_instrument=old_quote, order_proposal__trade_date=weekday, price=Decimal("100"), shares=Decimal("10")
112
+ )
113
+ o2 = order_factory.create( # noqa: F841
114
+ underlying_instrument=old_quote,
115
+ order_proposal__trade_date=(weekday + BDay(1)),
116
+ price=Decimal("100"),
117
+ shares=Decimal("10"),
118
+ )
119
+ o3 = order_factory.create(
120
+ underlying_instrument=old_quote,
121
+ order_proposal__trade_date=(weekday + BDay(1)),
122
+ price=Decimal("100"),
123
+ shares=Decimal("10"),
124
+ )
125
+ adjust_quote(
126
+ old_quote,
127
+ new_quote,
128
+ adjust_after=weekday,
129
+ only_portfolios=Portfolio.objects.filter(id=o3.order_proposal.portfolio.id),
130
+ )
131
+
132
+ mock_adjust_assets.asser_called_once()
133
+ assert set(mock_adjust_assets.call_args[0][0]) == set(AssetPosition.objects.none())
134
+ assert mock_adjust_assets.call_args[0][1] == new_quote
135
+
136
+ mock_adjust_orders.assert_called_once()
137
+ assert set(mock_adjust_orders.call_args[0][0]) == set(Order.objects.filter(id=o3.id))
138
+ assert mock_adjust_orders.call_args[0][1] == new_quote
139
+
140
+ mock_replay.assert_called_once_with(reapply_order_proposal=True)
@@ -2,7 +2,6 @@ from datetime import timedelta
2
2
  from decimal import Decimal
3
3
 
4
4
  import pytest
5
- from wbfdm.factories import CashFactory
6
5
  from wbfdm.models import InstrumentPrice
7
6
 
8
7
  from wbportfolio.models import FeeCalculation, Fees, Product
@@ -11,19 +10,14 @@ from wbportfolio.models import FeeCalculation, Fees, Product
11
10
  def fees_calculation(price_id):
12
11
  price = InstrumentPrice.objects.get(id=price_id)
13
12
  product = Product.objects.get(id=price.instrument.id)
14
- cash = CashFactory.create(currency=product.currency)
15
13
  yield {
16
- "portfolio": product.primary_portfolio,
17
- "linked_product": product,
18
- "transaction_date": price.date,
14
+ "product": product,
15
+ "fee_date": price.date,
19
16
  "transaction_subtype": Fees.Type.MANAGEMENT,
20
- "underlying_instrument": cash,
21
17
  "currency": product.currency,
22
18
  "calculated": True,
23
19
  "total_value": price.net_value,
24
- "total_value_fx_portfolio": price.net_value,
25
20
  "total_value_gross": price.net_value,
26
- "total_value_gross_fx_portfolio": price.net_value,
27
21
  }
28
22
 
29
23
 
@@ -38,14 +32,14 @@ class TestFeesModel:
38
32
  ) # no matter if estimated or not, we expect this fee to be on the resulting queryset
39
33
  calculated_fees_d1 = fees_factory.create( # there will be a real fee for that date, type and product, so this will be filtered out
40
34
  calculated=True,
41
- linked_product=fees_d0.linked_product,
42
- transaction_date=(fees_d0.transaction_date + timedelta(days=1)),
35
+ product=fees_d0.product,
36
+ fee_date=(fees_d0.fee_date + timedelta(days=1)),
43
37
  transaction_subtype=fees_d0.transaction_subtype,
44
38
  )
45
39
  real_fees_d1 = fees_factory.create(
46
40
  calculated=False,
47
- linked_product=calculated_fees_d1.linked_product,
48
- transaction_date=calculated_fees_d1.transaction_date,
41
+ product=calculated_fees_d1.product,
42
+ fee_date=calculated_fees_d1.fee_date,
49
43
  transaction_subtype=calculated_fees_d1.transaction_subtype,
50
44
  )
51
45
 
@@ -61,6 +55,6 @@ class TestFeesModel:
61
55
  price = instrument_price_factory.create(instrument=product) # post save must be called to compute fees
62
56
 
63
57
  fees = Fees.objects.get(
64
- linked_product=product, fee_date=price.date, calculated=True, transaction_subtype=Fees.Type.MANAGEMENT
58
+ product=product, fee_date=price.date, calculated=True, transaction_subtype=Fees.Type.MANAGEMENT
65
59
  )
66
60
  assert fees.total_value == pytest.approx(price.net_value, rel=Decimal(1e-4))