wbportfolio 2.2.1__py2.py3-none-any.whl

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

Potentially problematic release.


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

Files changed (486) hide show
  1. wbportfolio/__init__.py +1 -0
  2. wbportfolio/admin/__init__.py +12 -0
  3. wbportfolio/admin/asset.py +47 -0
  4. wbportfolio/admin/custodians.py +9 -0
  5. wbportfolio/admin/portfolio.py +127 -0
  6. wbportfolio/admin/portfolio_relationships.py +22 -0
  7. wbportfolio/admin/product_groups.py +42 -0
  8. wbportfolio/admin/products.py +80 -0
  9. wbportfolio/admin/reconciliations.py +14 -0
  10. wbportfolio/admin/registers.py +17 -0
  11. wbportfolio/admin/roles.py +19 -0
  12. wbportfolio/admin/synchronization/__init__.py +2 -0
  13. wbportfolio/admin/synchronization/admin.py +114 -0
  14. wbportfolio/admin/synchronization/portfolio_synchronization.py +18 -0
  15. wbportfolio/admin/synchronization/price_computation.py +21 -0
  16. wbportfolio/admin/transactions/__init__.py +5 -0
  17. wbportfolio/admin/transactions/claim.py +16 -0
  18. wbportfolio/admin/transactions/dividends.py +14 -0
  19. wbportfolio/admin/transactions/fees.py +35 -0
  20. wbportfolio/admin/transactions/trades.py +49 -0
  21. wbportfolio/admin/transactions/transactions.py +37 -0
  22. wbportfolio/analysis/__init__.py +0 -0
  23. wbportfolio/analysis/claims.py +235 -0
  24. wbportfolio/apps.py +5 -0
  25. wbportfolio/contrib/__init__.py +0 -0
  26. wbportfolio/contrib/company_portfolio/__init__.py +0 -0
  27. wbportfolio/contrib/company_portfolio/admin.py +28 -0
  28. wbportfolio/contrib/company_portfolio/apps.py +29 -0
  29. wbportfolio/contrib/company_portfolio/configs/__init__.py +3 -0
  30. wbportfolio/contrib/company_portfolio/configs/display.py +182 -0
  31. wbportfolio/contrib/company_portfolio/configs/endpoints.py +34 -0
  32. wbportfolio/contrib/company_portfolio/configs/previews.py +37 -0
  33. wbportfolio/contrib/company_portfolio/constants.py +1 -0
  34. wbportfolio/contrib/company_portfolio/dynamic_preferences_registry.py +87 -0
  35. wbportfolio/contrib/company_portfolio/factories.py +32 -0
  36. wbportfolio/contrib/company_portfolio/filters.py +127 -0
  37. wbportfolio/contrib/company_portfolio/management.py +19 -0
  38. wbportfolio/contrib/company_portfolio/migrations/0001_initial.py +214 -0
  39. wbportfolio/contrib/company_portfolio/migrations/__init__.py +0 -0
  40. wbportfolio/contrib/company_portfolio/models.py +334 -0
  41. wbportfolio/contrib/company_portfolio/scripts.py +76 -0
  42. wbportfolio/contrib/company_portfolio/serializers.py +303 -0
  43. wbportfolio/contrib/company_portfolio/tasks.py +19 -0
  44. wbportfolio/contrib/company_portfolio/tests/__init__.py +0 -0
  45. wbportfolio/contrib/company_portfolio/tests/conftest.py +161 -0
  46. wbportfolio/contrib/company_portfolio/tests/test_models.py +161 -0
  47. wbportfolio/contrib/company_portfolio/urls.py +29 -0
  48. wbportfolio/contrib/company_portfolio/viewsets.py +195 -0
  49. wbportfolio/defaults/__init__.py +0 -0
  50. wbportfolio/defaults/fees/__init__.py +0 -0
  51. wbportfolio/defaults/fees/default.py +92 -0
  52. wbportfolio/defaults/portfolio/__init__.py +0 -0
  53. wbportfolio/defaults/portfolio/default_rebalancing.py +45 -0
  54. wbportfolio/dynamic_preferences_registry.py +58 -0
  55. wbportfolio/factories/__init__.py +35 -0
  56. wbportfolio/factories/adjustments.py +17 -0
  57. wbportfolio/factories/assets.py +75 -0
  58. wbportfolio/factories/claim.py +39 -0
  59. wbportfolio/factories/custodians.py +11 -0
  60. wbportfolio/factories/dividends.py +14 -0
  61. wbportfolio/factories/fees.py +15 -0
  62. wbportfolio/factories/indexes.py +17 -0
  63. wbportfolio/factories/portfolio_cash_flow.py +20 -0
  64. wbportfolio/factories/portfolio_cash_targets.py +15 -0
  65. wbportfolio/factories/portfolio_swing_pricings.py +15 -0
  66. wbportfolio/factories/portfolios.py +59 -0
  67. wbportfolio/factories/product_groups.py +28 -0
  68. wbportfolio/factories/products.py +56 -0
  69. wbportfolio/factories/pytest_utils.py +121 -0
  70. wbportfolio/factories/reconciliations.py +23 -0
  71. wbportfolio/factories/roles.py +20 -0
  72. wbportfolio/factories/synchronization.py +40 -0
  73. wbportfolio/factories/trades.py +35 -0
  74. wbportfolio/factories/transactions.py +21 -0
  75. wbportfolio/fdm/__init__.py +0 -0
  76. wbportfolio/fdm/tasks.py +12 -0
  77. wbportfolio/filters/__init__.py +32 -0
  78. wbportfolio/filters/assets.py +485 -0
  79. wbportfolio/filters/assets_and_net_new_money_progression.py +42 -0
  80. wbportfolio/filters/custodians.py +10 -0
  81. wbportfolio/filters/esg.py +22 -0
  82. wbportfolio/filters/performances.py +171 -0
  83. wbportfolio/filters/portfolios.py +24 -0
  84. wbportfolio/filters/positions.py +178 -0
  85. wbportfolio/filters/products.py +157 -0
  86. wbportfolio/filters/roles.py +26 -0
  87. wbportfolio/filters/signals.py +92 -0
  88. wbportfolio/filters/transactions/__init__.py +20 -0
  89. wbportfolio/filters/transactions/claim.py +394 -0
  90. wbportfolio/filters/transactions/fees.py +66 -0
  91. wbportfolio/filters/transactions/trades.py +224 -0
  92. wbportfolio/filters/transactions/transactions.py +98 -0
  93. wbportfolio/import_export/__init__.py +0 -0
  94. wbportfolio/import_export/backends/__init__.py +2 -0
  95. wbportfolio/import_export/backends/ubs/__init__.py +3 -0
  96. wbportfolio/import_export/backends/ubs/asset_position.py +45 -0
  97. wbportfolio/import_export/backends/ubs/fees.py +63 -0
  98. wbportfolio/import_export/backends/ubs/instrument_price.py +44 -0
  99. wbportfolio/import_export/backends/ubs/mixin.py +15 -0
  100. wbportfolio/import_export/backends/utils.py +58 -0
  101. wbportfolio/import_export/backends/wbfdm/__init__.py +2 -0
  102. wbportfolio/import_export/backends/wbfdm/adjustment.py +50 -0
  103. wbportfolio/import_export/backends/wbfdm/dividend.py +16 -0
  104. wbportfolio/import_export/backends/wbfdm/mixin.py +15 -0
  105. wbportfolio/import_export/handlers/__init__.py +0 -0
  106. wbportfolio/import_export/handlers/adjustment.py +39 -0
  107. wbportfolio/import_export/handlers/asset_position.py +167 -0
  108. wbportfolio/import_export/handlers/dividend.py +80 -0
  109. wbportfolio/import_export/handlers/fees.py +58 -0
  110. wbportfolio/import_export/handlers/portfolio_cash_flow.py +57 -0
  111. wbportfolio/import_export/handlers/register.py +43 -0
  112. wbportfolio/import_export/handlers/trade.py +191 -0
  113. wbportfolio/import_export/parsers/__init__.py +0 -0
  114. wbportfolio/import_export/parsers/default_mapping.py +30 -0
  115. wbportfolio/import_export/parsers/jpmorgan/__init__.py +0 -0
  116. wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +63 -0
  117. wbportfolio/import_export/parsers/jpmorgan/fees.py +64 -0
  118. wbportfolio/import_export/parsers/jpmorgan/strategy.py +116 -0
  119. wbportfolio/import_export/parsers/jpmorgan/valuation.py +41 -0
  120. wbportfolio/import_export/parsers/leonteq/__init__.py +0 -0
  121. wbportfolio/import_export/parsers/leonteq/customer_trade.py +47 -0
  122. wbportfolio/import_export/parsers/leonteq/equity.py +81 -0
  123. wbportfolio/import_export/parsers/leonteq/fees.py +70 -0
  124. wbportfolio/import_export/parsers/leonteq/trade.py +94 -0
  125. wbportfolio/import_export/parsers/leonteq/valuation.py +39 -0
  126. wbportfolio/import_export/parsers/natixis/__init__.py +0 -0
  127. wbportfolio/import_export/parsers/natixis/customer_trade.py +62 -0
  128. wbportfolio/import_export/parsers/natixis/d1_customer_trade.py +66 -0
  129. wbportfolio/import_export/parsers/natixis/d1_equity.py +80 -0
  130. wbportfolio/import_export/parsers/natixis/d1_fees.py +58 -0
  131. wbportfolio/import_export/parsers/natixis/d1_trade.py +70 -0
  132. wbportfolio/import_export/parsers/natixis/d1_valuation.py +41 -0
  133. wbportfolio/import_export/parsers/natixis/dividend.py +53 -0
  134. wbportfolio/import_export/parsers/natixis/equity.py +60 -0
  135. wbportfolio/import_export/parsers/natixis/fees.py +53 -0
  136. wbportfolio/import_export/parsers/natixis/trade.py +63 -0
  137. wbportfolio/import_export/parsers/natixis/utils.py +76 -0
  138. wbportfolio/import_export/parsers/natixis/valuation.py +46 -0
  139. wbportfolio/import_export/parsers/refinitiv/__init__.py +0 -0
  140. wbportfolio/import_export/parsers/refinitiv/adjustment.py +24 -0
  141. wbportfolio/import_export/parsers/sg_lux/__init__.py +0 -0
  142. wbportfolio/import_export/parsers/sg_lux/custodian_positions.py +70 -0
  143. wbportfolio/import_export/parsers/sg_lux/customer_trade.py +75 -0
  144. wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +140 -0
  145. wbportfolio/import_export/parsers/sg_lux/customer_trade_slk.py +80 -0
  146. wbportfolio/import_export/parsers/sg_lux/customer_trade_without_pw.py +57 -0
  147. wbportfolio/import_export/parsers/sg_lux/equity.py +137 -0
  148. wbportfolio/import_export/parsers/sg_lux/fees.py +56 -0
  149. wbportfolio/import_export/parsers/sg_lux/perf_fees.py +51 -0
  150. wbportfolio/import_export/parsers/sg_lux/portfolio_cash_flow.py +29 -0
  151. wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py +36 -0
  152. wbportfolio/import_export/parsers/sg_lux/registers.py +210 -0
  153. wbportfolio/import_export/parsers/sg_lux/sylk.py +248 -0
  154. wbportfolio/import_export/parsers/sg_lux/utils.py +36 -0
  155. wbportfolio/import_export/parsers/sg_lux/valuation.py +53 -0
  156. wbportfolio/import_export/parsers/societe_generale/__init__.py +0 -0
  157. wbportfolio/import_export/parsers/societe_generale/customer_trade.py +54 -0
  158. wbportfolio/import_export/parsers/societe_generale/strategy.py +94 -0
  159. wbportfolio/import_export/parsers/societe_generale/valuation.py +37 -0
  160. wbportfolio/import_export/parsers/tellco/__init__.py +0 -0
  161. wbportfolio/import_export/parsers/tellco/customer_trade.py +64 -0
  162. wbportfolio/import_export/parsers/tellco/equity.py +86 -0
  163. wbportfolio/import_export/parsers/tellco/valuation.py +52 -0
  164. wbportfolio/import_export/parsers/ubs/__init__.py +0 -0
  165. wbportfolio/import_export/parsers/ubs/api/__init__.py +0 -0
  166. wbportfolio/import_export/parsers/ubs/api/asset_position.py +106 -0
  167. wbportfolio/import_export/parsers/ubs/api/fees.py +31 -0
  168. wbportfolio/import_export/parsers/ubs/api/instrument_price.py +20 -0
  169. wbportfolio/import_export/parsers/ubs/api/utils.py +0 -0
  170. wbportfolio/import_export/parsers/ubs/customer_trade.py +60 -0
  171. wbportfolio/import_export/parsers/ubs/equity.py +97 -0
  172. wbportfolio/import_export/parsers/ubs/historical_customer_trade.py +67 -0
  173. wbportfolio/import_export/parsers/ubs/valuation.py +52 -0
  174. wbportfolio/import_export/parsers/vontobel/__init__.py +0 -0
  175. wbportfolio/import_export/parsers/vontobel/asset_position.py +97 -0
  176. wbportfolio/import_export/parsers/vontobel/customer_trade.py +54 -0
  177. wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +40 -0
  178. wbportfolio/import_export/parsers/vontobel/instrument.py +34 -0
  179. wbportfolio/import_export/parsers/vontobel/management_fees.py +86 -0
  180. wbportfolio/import_export/parsers/vontobel/performance_fees.py +35 -0
  181. wbportfolio/import_export/parsers/vontobel/trade.py +38 -0
  182. wbportfolio/import_export/parsers/vontobel/utils.py +17 -0
  183. wbportfolio/import_export/parsers/vontobel/valuation.py +29 -0
  184. wbportfolio/import_export/resources/__init__.py +0 -0
  185. wbportfolio/import_export/resources/assets.py +68 -0
  186. wbportfolio/import_export/resources/trades.py +41 -0
  187. wbportfolio/import_export/utils.py +42 -0
  188. wbportfolio/metric/__init__.py +0 -0
  189. wbportfolio/metric/backends/__init__.py +2 -0
  190. wbportfolio/metric/backends/base.py +86 -0
  191. wbportfolio/metric/backends/constants.py +222 -0
  192. wbportfolio/metric/backends/portfolio_base.py +255 -0
  193. wbportfolio/metric/backends/portfolio_esg.py +66 -0
  194. wbportfolio/metric/tests/__init__.py +0 -0
  195. wbportfolio/metric/tests/conftest.py +4 -0
  196. wbportfolio/metric/tests/test_portfolio_base.py +135 -0
  197. wbportfolio/metric/tests/test_portfolio_esg.py +69 -0
  198. wbportfolio/migrations/0001_initial_squashed.py +13848 -0
  199. wbportfolio/migrations/0002_product_default_sub_account_squashed_0039_alter_assetallocation_company_and_more.py +3836 -0
  200. wbportfolio/migrations/0040_instrument_financial_instrument.py +26 -0
  201. wbportfolio/migrations/0041_remove_listresearch_research_ptr_and_more.py +129 -0
  202. wbportfolio/migrations/0042_instrumentlist_instrumentlistthroughmodel_and_more.py +71 -0
  203. wbportfolio/migrations/0043_alter_instrumentlistthroughmodel_options_and_more.py +238 -0
  204. wbportfolio/migrations/0044_alter_instrumentlist_identifier.py +35 -0
  205. wbportfolio/migrations/0045_alter_instrument_financial_instrument.py +26 -0
  206. wbportfolio/migrations/0046_add_product_default_account.py +166 -0
  207. wbportfolio/migrations/0047_remove_product_default_sub_account.py +14 -0
  208. wbportfolio/migrations/0048_alter_trade_status.py +29 -0
  209. wbportfolio/migrations/0049_trade_claimed_shares.py +25 -0
  210. wbportfolio/migrations/0050_fees_fee_date_fees_wbportfolio_transac_1f7a29_idx.py +44 -0
  211. wbportfolio/migrations/0051_delete_macroreview.py +11 -0
  212. wbportfolio/migrations/0052_remove_cash_instrument_ptr_and_more.py +888 -0
  213. wbportfolio/migrations/0053_remove_product_group.py +132 -0
  214. wbportfolio/migrations/0054_portfolioinstrumentpreferredclassificationthroughmodel_and_more.py +270 -0
  215. wbportfolio/migrations/0055_remove_product__custom_management_rebates_and_more.py +139 -0
  216. wbportfolio/migrations/0056_remove_companyportfoliodata_assets_under_management_currency_and_more.py +56 -0
  217. wbportfolio/migrations/0057_alter_portfolio_preferred_instrument_classifications_and_more.py +36 -0
  218. wbportfolio/migrations/0058_pmsinstrument.py +23 -0
  219. wbportfolio/migrations/0059_fees_unique_fees.py +51 -0
  220. wbportfolio/migrations/0060_alter_portfolioportfoliothroughmodel_type.py +21 -0
  221. wbportfolio/migrations/0061_portfolio_bank_accounts_product_bank_account_and_more.py +175 -0
  222. wbportfolio/migrations/0062_alter_dailyportfoliocashflow_options.py +20 -0
  223. wbportfolio/migrations/0063_accountreconciliation_accountreconciliationline_and_more.py +133 -0
  224. wbportfolio/migrations/0064_alter_portfolio_managers_portfolio_is_tracked_and_more.py +40 -0
  225. wbportfolio/migrations/0065_alter_portfolio_managers_claim_as_shares_and_more.py +73 -0
  226. wbportfolio/migrations/0066_assetposition_initial_shares_at_custodian_and_more.py +108 -0
  227. wbportfolio/migrations/0067_assetposition_unique_asset_position.py +77 -0
  228. wbportfolio/migrations/0068_trade_internal_trade_trade_marked_as_internal_and_more.py +59 -0
  229. wbportfolio/migrations/0069_remove_portfolio_is_invested_and_more.py +56 -0
  230. wbportfolio/migrations/0070_remove_assetposition_unique_asset_position_and_more.py +82 -0
  231. wbportfolio/migrations/0071_alter_trade_options_alter_trade_order.py +22 -0
  232. wbportfolio/migrations/__init__.py +0 -0
  233. wbportfolio/models/__init__.py +26 -0
  234. wbportfolio/models/adjustments.py +246 -0
  235. wbportfolio/models/asset.py +869 -0
  236. wbportfolio/models/custodians.py +101 -0
  237. wbportfolio/models/indexes.py +33 -0
  238. wbportfolio/models/mixins/__init__.py +0 -0
  239. wbportfolio/models/mixins/instruments.py +127 -0
  240. wbportfolio/models/mixins/liquidity_stress_test.py +1307 -0
  241. wbportfolio/models/portfolio.py +1039 -0
  242. wbportfolio/models/portfolio_cash_flow.py +167 -0
  243. wbportfolio/models/portfolio_cash_targets.py +46 -0
  244. wbportfolio/models/portfolio_relationship.py +135 -0
  245. wbportfolio/models/portfolio_swing_pricings.py +51 -0
  246. wbportfolio/models/product_groups.py +230 -0
  247. wbportfolio/models/products.py +569 -0
  248. wbportfolio/models/reconciliations/__init__.py +2 -0
  249. wbportfolio/models/reconciliations/account_reconciliation_lines.py +192 -0
  250. wbportfolio/models/reconciliations/account_reconciliations.py +102 -0
  251. wbportfolio/models/reconciliations/reconciliations.py +25 -0
  252. wbportfolio/models/registers.py +132 -0
  253. wbportfolio/models/roles.py +208 -0
  254. wbportfolio/models/synchronization/__init__.py +3 -0
  255. wbportfolio/models/synchronization/portfolio_synchronization.py +292 -0
  256. wbportfolio/models/synchronization/price_computation.py +200 -0
  257. wbportfolio/models/synchronization/synchronization.py +188 -0
  258. wbportfolio/models/transactions/__init__.py +7 -0
  259. wbportfolio/models/transactions/claim.py +634 -0
  260. wbportfolio/models/transactions/dividends.py +31 -0
  261. wbportfolio/models/transactions/expiry.py +7 -0
  262. wbportfolio/models/transactions/fees.py +153 -0
  263. wbportfolio/models/transactions/trade_proposals.py +502 -0
  264. wbportfolio/models/transactions/trades.py +704 -0
  265. wbportfolio/models/transactions/transactions.py +211 -0
  266. wbportfolio/models/utils.py +12 -0
  267. wbportfolio/permissions.py +13 -0
  268. wbportfolio/pms/__init__.py +0 -0
  269. wbportfolio/pms/statistics/__init__.py +0 -0
  270. wbportfolio/pms/trading/__init__.py +1 -0
  271. wbportfolio/pms/trading/handler.py +164 -0
  272. wbportfolio/pms/typing.py +194 -0
  273. wbportfolio/preferences.py +6 -0
  274. wbportfolio/reports/__init__.py +0 -0
  275. wbportfolio/reports/monthly_position_report.py +74 -0
  276. wbportfolio/risk_management/__init__.py +0 -0
  277. wbportfolio/risk_management/backends/__init__.py +11 -0
  278. wbportfolio/risk_management/backends/accounts.py +166 -0
  279. wbportfolio/risk_management/backends/controversy_portfolio.py +63 -0
  280. wbportfolio/risk_management/backends/exposure_portfolio.py +203 -0
  281. wbportfolio/risk_management/backends/instrument_list_portfolio.py +89 -0
  282. wbportfolio/risk_management/backends/liquidity_risk.py +86 -0
  283. wbportfolio/risk_management/backends/liquidity_stress_instrument.py +86 -0
  284. wbportfolio/risk_management/backends/mixins.py +220 -0
  285. wbportfolio/risk_management/backends/product_integrity.py +111 -0
  286. wbportfolio/risk_management/backends/stop_loss_instrument.py +24 -0
  287. wbportfolio/risk_management/backends/stop_loss_portfolio.py +36 -0
  288. wbportfolio/risk_management/backends/ucits_portfolio.py +63 -0
  289. wbportfolio/risk_management/tests/__init__.py +0 -0
  290. wbportfolio/risk_management/tests/conftest.py +15 -0
  291. wbportfolio/risk_management/tests/test_accounts.py +98 -0
  292. wbportfolio/risk_management/tests/test_controversy_portfolio.py +33 -0
  293. wbportfolio/risk_management/tests/test_exposure_portfolio.py +94 -0
  294. wbportfolio/risk_management/tests/test_instrument_list_portfolio.py +60 -0
  295. wbportfolio/risk_management/tests/test_liquidity_risk.py +47 -0
  296. wbportfolio/risk_management/tests/test_product_integrity.py +55 -0
  297. wbportfolio/risk_management/tests/test_stop_loss_instrument.py +110 -0
  298. wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +119 -0
  299. wbportfolio/risk_management/tests/test_ucits_portfolio.py +39 -0
  300. wbportfolio/serializers/__init__.py +42 -0
  301. wbportfolio/serializers/adjustments.py +24 -0
  302. wbportfolio/serializers/assets.py +166 -0
  303. wbportfolio/serializers/custodians.py +26 -0
  304. wbportfolio/serializers/portfolio_cash_flow.py +48 -0
  305. wbportfolio/serializers/portfolio_cash_targets.py +20 -0
  306. wbportfolio/serializers/portfolio_relationship.py +53 -0
  307. wbportfolio/serializers/portfolio_swing_pricing.py +20 -0
  308. wbportfolio/serializers/portfolios.py +143 -0
  309. wbportfolio/serializers/positions.py +76 -0
  310. wbportfolio/serializers/product_group.py +88 -0
  311. wbportfolio/serializers/products.py +331 -0
  312. wbportfolio/serializers/reconciliations.py +171 -0
  313. wbportfolio/serializers/registers.py +72 -0
  314. wbportfolio/serializers/roles.py +60 -0
  315. wbportfolio/serializers/signals.py +157 -0
  316. wbportfolio/serializers/synchronization.py +18 -0
  317. wbportfolio/serializers/transactions/__init__.py +24 -0
  318. wbportfolio/serializers/transactions/claim.py +310 -0
  319. wbportfolio/serializers/transactions/dividends.py +18 -0
  320. wbportfolio/serializers/transactions/expiry.py +18 -0
  321. wbportfolio/serializers/transactions/fees.py +32 -0
  322. wbportfolio/serializers/transactions/trades.py +315 -0
  323. wbportfolio/serializers/transactions/transactions.py +84 -0
  324. wbportfolio/tasks.py +125 -0
  325. wbportfolio/tests/__init__.py +0 -0
  326. wbportfolio/tests/conftest.py +164 -0
  327. wbportfolio/tests/models/__init__.py +0 -0
  328. wbportfolio/tests/models/test_account_reconciliation.py +191 -0
  329. wbportfolio/tests/models/test_assets.py +193 -0
  330. wbportfolio/tests/models/test_custodians.py +12 -0
  331. wbportfolio/tests/models/test_customer_trades.py +113 -0
  332. wbportfolio/tests/models/test_dividends.py +7 -0
  333. wbportfolio/tests/models/test_imports.py +192 -0
  334. wbportfolio/tests/models/test_instrument_mixins.py +48 -0
  335. wbportfolio/tests/models/test_merge.py +133 -0
  336. wbportfolio/tests/models/test_portfolio_cash_flow.py +112 -0
  337. wbportfolio/tests/models/test_portfolio_cash_targets.py +27 -0
  338. wbportfolio/tests/models/test_portfolio_swing_pricings.py +42 -0
  339. wbportfolio/tests/models/test_portfolios.py +676 -0
  340. wbportfolio/tests/models/test_product_groups.py +80 -0
  341. wbportfolio/tests/models/test_products.py +187 -0
  342. wbportfolio/tests/models/test_roles.py +82 -0
  343. wbportfolio/tests/models/test_splits.py +233 -0
  344. wbportfolio/tests/models/test_synchronization.py +617 -0
  345. wbportfolio/tests/models/transactions/__init__.py +0 -0
  346. wbportfolio/tests/models/transactions/test_claim.py +129 -0
  347. wbportfolio/tests/models/transactions/test_fees.py +65 -0
  348. wbportfolio/tests/models/transactions/test_trades.py +204 -0
  349. wbportfolio/tests/models/utils.py +13 -0
  350. wbportfolio/tests/serializers/__init__.py +0 -0
  351. wbportfolio/tests/serializers/test_claims.py +21 -0
  352. wbportfolio/tests/signals.py +151 -0
  353. wbportfolio/tests/tests.py +31 -0
  354. wbportfolio/tests/viewsets/__init__.py +0 -0
  355. wbportfolio/tests/viewsets/test_assets.py +67 -0
  356. wbportfolio/tests/viewsets/test_performances.py +72 -0
  357. wbportfolio/tests/viewsets/test_products.py +92 -0
  358. wbportfolio/tests/viewsets/transactions/__init__.py +0 -0
  359. wbportfolio/tests/viewsets/transactions/test_claims.py +146 -0
  360. wbportfolio/urls.py +247 -0
  361. wbportfolio/utils.py +30 -0
  362. wbportfolio/viewsets/__init__.py +57 -0
  363. wbportfolio/viewsets/adjustments.py +46 -0
  364. wbportfolio/viewsets/assets.py +562 -0
  365. wbportfolio/viewsets/assets_and_net_new_money_progression.py +117 -0
  366. wbportfolio/viewsets/charts/__init__.py +1 -0
  367. wbportfolio/viewsets/charts/assets.py +247 -0
  368. wbportfolio/viewsets/configs/__init__.py +6 -0
  369. wbportfolio/viewsets/configs/buttons/__init__.py +23 -0
  370. wbportfolio/viewsets/configs/buttons/adjustments.py +13 -0
  371. wbportfolio/viewsets/configs/buttons/assets.py +145 -0
  372. wbportfolio/viewsets/configs/buttons/claims.py +83 -0
  373. wbportfolio/viewsets/configs/buttons/custodians.py +76 -0
  374. wbportfolio/viewsets/configs/buttons/fees.py +14 -0
  375. wbportfolio/viewsets/configs/buttons/mixins.py +88 -0
  376. wbportfolio/viewsets/configs/buttons/portfolios.py +115 -0
  377. wbportfolio/viewsets/configs/buttons/products.py +41 -0
  378. wbportfolio/viewsets/configs/buttons/reconciliations.py +65 -0
  379. wbportfolio/viewsets/configs/buttons/registers.py +11 -0
  380. wbportfolio/viewsets/configs/buttons/signals.py +68 -0
  381. wbportfolio/viewsets/configs/buttons/trade_proposals.py +25 -0
  382. wbportfolio/viewsets/configs/buttons/trades.py +144 -0
  383. wbportfolio/viewsets/configs/display/__init__.py +61 -0
  384. wbportfolio/viewsets/configs/display/adjustments.py +81 -0
  385. wbportfolio/viewsets/configs/display/assets.py +265 -0
  386. wbportfolio/viewsets/configs/display/claim.py +299 -0
  387. wbportfolio/viewsets/configs/display/custodians.py +24 -0
  388. wbportfolio/viewsets/configs/display/esg.py +88 -0
  389. wbportfolio/viewsets/configs/display/fees.py +133 -0
  390. wbportfolio/viewsets/configs/display/portfolio_cash_flow.py +103 -0
  391. wbportfolio/viewsets/configs/display/portfolio_relationship.py +38 -0
  392. wbportfolio/viewsets/configs/display/portfolios.py +125 -0
  393. wbportfolio/viewsets/configs/display/positions.py +75 -0
  394. wbportfolio/viewsets/configs/display/product_groups.py +54 -0
  395. wbportfolio/viewsets/configs/display/product_performance.py +241 -0
  396. wbportfolio/viewsets/configs/display/products.py +249 -0
  397. wbportfolio/viewsets/configs/display/reconciliations.py +151 -0
  398. wbportfolio/viewsets/configs/display/registers.py +71 -0
  399. wbportfolio/viewsets/configs/display/roles.py +49 -0
  400. wbportfolio/viewsets/configs/display/trade_proposals.py +97 -0
  401. wbportfolio/viewsets/configs/display/trades.py +359 -0
  402. wbportfolio/viewsets/configs/display/transactions.py +55 -0
  403. wbportfolio/viewsets/configs/endpoints/__init__.py +75 -0
  404. wbportfolio/viewsets/configs/endpoints/adjustments.py +17 -0
  405. wbportfolio/viewsets/configs/endpoints/assets.py +115 -0
  406. wbportfolio/viewsets/configs/endpoints/claim.py +106 -0
  407. wbportfolio/viewsets/configs/endpoints/custodians.py +6 -0
  408. wbportfolio/viewsets/configs/endpoints/esg.py +14 -0
  409. wbportfolio/viewsets/configs/endpoints/fees.py +26 -0
  410. wbportfolio/viewsets/configs/endpoints/portfolio_relationship.py +23 -0
  411. wbportfolio/viewsets/configs/endpoints/portfolios.py +43 -0
  412. wbportfolio/viewsets/configs/endpoints/positions.py +18 -0
  413. wbportfolio/viewsets/configs/endpoints/product_groups.py +11 -0
  414. wbportfolio/viewsets/configs/endpoints/product_performance.py +29 -0
  415. wbportfolio/viewsets/configs/endpoints/products.py +37 -0
  416. wbportfolio/viewsets/configs/endpoints/reconciliations.py +31 -0
  417. wbportfolio/viewsets/configs/endpoints/roles.py +9 -0
  418. wbportfolio/viewsets/configs/endpoints/trade_proposals.py +17 -0
  419. wbportfolio/viewsets/configs/endpoints/trades.py +82 -0
  420. wbportfolio/viewsets/configs/endpoints/transactions.py +17 -0
  421. wbportfolio/viewsets/configs/menu/__init__.py +30 -0
  422. wbportfolio/viewsets/configs/menu/adjustments.py +8 -0
  423. wbportfolio/viewsets/configs/menu/assets.py +8 -0
  424. wbportfolio/viewsets/configs/menu/claim.py +41 -0
  425. wbportfolio/viewsets/configs/menu/custodians.py +11 -0
  426. wbportfolio/viewsets/configs/menu/fees.py +13 -0
  427. wbportfolio/viewsets/configs/menu/instrument_prices.py +10 -0
  428. wbportfolio/viewsets/configs/menu/portfolio_cash_flow.py +8 -0
  429. wbportfolio/viewsets/configs/menu/portfolios.py +15 -0
  430. wbportfolio/viewsets/configs/menu/positions.py +14 -0
  431. wbportfolio/viewsets/configs/menu/product_groups.py +10 -0
  432. wbportfolio/viewsets/configs/menu/product_performance.py +25 -0
  433. wbportfolio/viewsets/configs/menu/products.py +15 -0
  434. wbportfolio/viewsets/configs/menu/reconciliations.py +7 -0
  435. wbportfolio/viewsets/configs/menu/registers.py +10 -0
  436. wbportfolio/viewsets/configs/menu/roles.py +16 -0
  437. wbportfolio/viewsets/configs/menu/trades.py +18 -0
  438. wbportfolio/viewsets/configs/menu/transactions.py +8 -0
  439. wbportfolio/viewsets/configs/previews/__init__.py +1 -0
  440. wbportfolio/viewsets/configs/previews/portfolios.py +21 -0
  441. wbportfolio/viewsets/configs/titles/__init__.py +65 -0
  442. wbportfolio/viewsets/configs/titles/adjustments.py +19 -0
  443. wbportfolio/viewsets/configs/titles/assets.py +57 -0
  444. wbportfolio/viewsets/configs/titles/assets_and_net_new_money_progression.py +6 -0
  445. wbportfolio/viewsets/configs/titles/claim.py +81 -0
  446. wbportfolio/viewsets/configs/titles/custodians.py +12 -0
  447. wbportfolio/viewsets/configs/titles/esg.py +10 -0
  448. wbportfolio/viewsets/configs/titles/fees.py +25 -0
  449. wbportfolio/viewsets/configs/titles/instrument_prices.py +20 -0
  450. wbportfolio/viewsets/configs/titles/portfolios.py +32 -0
  451. wbportfolio/viewsets/configs/titles/positions.py +11 -0
  452. wbportfolio/viewsets/configs/titles/product_groups.py +12 -0
  453. wbportfolio/viewsets/configs/titles/product_performance.py +16 -0
  454. wbportfolio/viewsets/configs/titles/products.py +6 -0
  455. wbportfolio/viewsets/configs/titles/registers.py +12 -0
  456. wbportfolio/viewsets/configs/titles/roles.py +23 -0
  457. wbportfolio/viewsets/configs/titles/trades.py +51 -0
  458. wbportfolio/viewsets/configs/titles/transactions.py +8 -0
  459. wbportfolio/viewsets/custodians.py +66 -0
  460. wbportfolio/viewsets/esg.py +165 -0
  461. wbportfolio/viewsets/mixins.py +48 -0
  462. wbportfolio/viewsets/portfolio_cash_flow.py +31 -0
  463. wbportfolio/viewsets/portfolio_cash_targets.py +8 -0
  464. wbportfolio/viewsets/portfolio_relationship.py +46 -0
  465. wbportfolio/viewsets/portfolio_swing_pricing.py +8 -0
  466. wbportfolio/viewsets/portfolios.py +154 -0
  467. wbportfolio/viewsets/positions.py +292 -0
  468. wbportfolio/viewsets/product_groups.py +84 -0
  469. wbportfolio/viewsets/product_performance.py +646 -0
  470. wbportfolio/viewsets/products.py +529 -0
  471. wbportfolio/viewsets/reconciliations.py +160 -0
  472. wbportfolio/viewsets/registers.py +75 -0
  473. wbportfolio/viewsets/roles.py +44 -0
  474. wbportfolio/viewsets/signals.py +42 -0
  475. wbportfolio/viewsets/synchronization.py +25 -0
  476. wbportfolio/viewsets/transactions/__init__.py +40 -0
  477. wbportfolio/viewsets/transactions/claim.py +933 -0
  478. wbportfolio/viewsets/transactions/fees.py +190 -0
  479. wbportfolio/viewsets/transactions/mixins.py +19 -0
  480. wbportfolio/viewsets/transactions/trade_proposals.py +93 -0
  481. wbportfolio/viewsets/transactions/trades.py +395 -0
  482. wbportfolio/viewsets/transactions/transactions.py +123 -0
  483. wbportfolio-2.2.1.dist-info/METADATA +21 -0
  484. wbportfolio-2.2.1.dist-info/RECORD +486 -0
  485. wbportfolio-2.2.1.dist-info/WHEEL +5 -0
  486. wbportfolio-2.2.1.dist-info/licenses/LICENSE +4 -0
@@ -0,0 +1,704 @@
1
+ from contextlib import suppress
2
+ from datetime import date, timedelta
3
+ from decimal import Decimal
4
+
5
+ from celery import shared_task
6
+ from django.contrib import admin
7
+ from django.db import models
8
+ from django.db.models import (
9
+ Case,
10
+ DateField,
11
+ ExpressionWrapper,
12
+ F,
13
+ OuterRef,
14
+ Q,
15
+ Subquery,
16
+ Sum,
17
+ When,
18
+ )
19
+ from django.db.models.functions import Coalesce
20
+ from django.db.models.signals import post_save
21
+ from django.dispatch import receiver
22
+ from django.utils.functional import cached_property
23
+ from django_fsm import GET_STATE, FSMField, transition
24
+ from ordered_model.models import OrderedModel, OrderedModelManager
25
+ from wbcore.contrib.icons import WBIcon
26
+ from wbcore.enums import RequestType
27
+ from wbcore.metadata.configs.buttons import ActionButton
28
+ from wbcore.models import WBModel
29
+ from wbcore.signals.models import pre_collection
30
+ from wbfdm.models.instruments.instrument_prices import InstrumentPrice
31
+ from wbportfolio.import_export.handlers.trade import TradeImportHandler
32
+ from wbportfolio.models.asset import AssetPosition
33
+ from wbportfolio.models.custodians import Custodian
34
+ from wbportfolio.models.roles import PortfolioRole
35
+ from wbportfolio.pms.typing import Trade as TradeDTO
36
+
37
+ from .transactions import ShareMixin, Transaction
38
+
39
+
40
+ class DefaultTradeManager(OrderedModelManager):
41
+ """This manager is expect to be the trade default manager and annotate by default the effective weight (extracted
42
+ from the associated portfolio) and the target weight as an addition between the effective weight and the delta weight
43
+ """
44
+
45
+ def get_queryset(self):
46
+ return (
47
+ super()
48
+ .get_queryset()
49
+ .annotate(
50
+ last_effective_date=Subquery(
51
+ AssetPosition.objects.filter(
52
+ underlying_instrument=OuterRef("underlying_instrument"),
53
+ date__lt=OuterRef("transaction_date"),
54
+ portfolio=OuterRef("portfolio"),
55
+ )
56
+ .order_by("-date")
57
+ .values("date")[:1]
58
+ ),
59
+ effective_weight=Coalesce(
60
+ Subquery(
61
+ AssetPosition.objects.filter(
62
+ underlying_instrument=OuterRef("underlying_instrument"),
63
+ date=OuterRef("last_effective_date"),
64
+ portfolio=OuterRef("portfolio"),
65
+ )
66
+ .values("portfolio")
67
+ .annotate(s=Sum("weighting"))
68
+ .values("s")[:1]
69
+ ),
70
+ Decimal(0),
71
+ ),
72
+ target_weight=F("effective_weight") + F("weighting"),
73
+ effective_shares=Coalesce(
74
+ Subquery(
75
+ AssetPosition.objects.filter(
76
+ underlying_instrument=OuterRef("underlying_instrument"),
77
+ date=OuterRef("last_effective_date"),
78
+ portfolio=OuterRef("portfolio"),
79
+ )
80
+ .values("portfolio")
81
+ .annotate(s=Sum("shares"))
82
+ .values("s")[:1]
83
+ ),
84
+ Decimal(0),
85
+ ),
86
+ target_shares=F("effective_weight") * F("weighting"),
87
+ diff_shares=F("shares") - F("claimed_shares"),
88
+ )
89
+ )
90
+
91
+
92
+ class ValidCustomerTradeManager(DefaultTradeManager):
93
+ def __init__(self, without_internal_trade: bool = False):
94
+ self.without_internal_trade = without_internal_trade
95
+ super().__init__()
96
+
97
+ def get_queryset(self):
98
+ qs = (
99
+ super()
100
+ .get_queryset()
101
+ .filter(
102
+ transaction_subtype__in=[Trade.Type.SUBSCRIPTION, Trade.Type.REDEMPTION],
103
+ marked_for_deletion=False,
104
+ pending=False,
105
+ )
106
+ )
107
+ if self.without_internal_trade:
108
+ qs = qs.exclude(marked_as_internal=True)
109
+ return qs
110
+
111
+
112
+ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
113
+ import_export_handler_class = TradeImportHandler
114
+
115
+ TRADE_WINDOW_INTERVAL = 7
116
+
117
+ class Status(models.TextChoices):
118
+ DRAFT = "DRAFT", "Draft"
119
+ SUBMIT = "SUBMIT", "Submit"
120
+ EXECUTED = "EXECUTED", "Executed"
121
+ CONFIRMED = "CONFIRMED", "Confirmed"
122
+ FAILED = "FAILED", "Failed"
123
+
124
+ class Type(models.TextChoices):
125
+ REBALANCE = "REBALANCE", "Rebalance"
126
+ DECREASE = "DECREASE", "Decrease"
127
+ INCREASE = "INCREASE", "Increase"
128
+ SUBSCRIPTION = "SUBSCRIPTION", "Subscription"
129
+ REDEMPTION = "REDEMPTION", "Redemption"
130
+ BUY = "BUY", "Buy"
131
+ SELL = "SELL", "Sell"
132
+
133
+ external_identifier2 = models.CharField(
134
+ max_length=255,
135
+ null=True,
136
+ blank=True,
137
+ help_text="A second external identifier that was supplied.",
138
+ verbose_name="External Identifier 2",
139
+ )
140
+
141
+ transaction_subtype = models.CharField(
142
+ max_length=32, default=Type.BUY, choices=Type.choices, verbose_name="Trade Type"
143
+ )
144
+ status = FSMField(default=Status.CONFIRMED, choices=Status.choices, verbose_name="Status")
145
+ weighting = models.DecimalField(
146
+ max_digits=16,
147
+ decimal_places=6,
148
+ default=Decimal(0),
149
+ help_text="The weight to be multiplied against the target",
150
+ verbose_name="Weight",
151
+ )
152
+ bank = models.CharField(
153
+ max_length=255,
154
+ help_text="The bank/counterparty/custodian the trade went through.",
155
+ verbose_name="Counterparty",
156
+ )
157
+ custodian = models.ForeignKey(
158
+ "wbportfolio.Custodian", null=True, blank=True, on_delete=models.SET_NULL, related_name="trades"
159
+ )
160
+ marked_for_deletion = models.BooleanField(
161
+ default=False,
162
+ help_text="If this is checked, then the trade is supposed to be deleted.",
163
+ verbose_name="To be deleted",
164
+ )
165
+
166
+ # Only valid for subscription and redemption trade
167
+ marked_as_internal = models.BooleanField(
168
+ default=False,
169
+ help_text="If this is checked, then this subscription or redemption is considered internal and will not be considered in any AUM computation",
170
+ verbose_name="Internal",
171
+ )
172
+ internal_trade = models.OneToOneField(
173
+ "wbportfolio.Trade",
174
+ null=True,
175
+ blank=True,
176
+ on_delete=models.SET_NULL,
177
+ related_name="internal_subscription_redemption_trade",
178
+ )
179
+
180
+ pending = models.BooleanField(default=False)
181
+ exclude_from_history = models.BooleanField(default=False)
182
+ register = models.ForeignKey(
183
+ to="wbportfolio.Register",
184
+ null=True,
185
+ blank=True,
186
+ related_name="trades",
187
+ on_delete=models.PROTECT,
188
+ )
189
+
190
+ trade_proposal = models.ForeignKey(
191
+ to="wbportfolio.TradeProposal",
192
+ null=True,
193
+ blank=True,
194
+ related_name="trades",
195
+ on_delete=models.CASCADE,
196
+ help_text="The Trade Proposal this trade is coming from",
197
+ )
198
+ claimed_shares = models.DecimalField(
199
+ max_digits=15,
200
+ decimal_places=4,
201
+ default=Decimal(0),
202
+ help_text="The number of shares that were claimed.",
203
+ verbose_name="Claimed Shares",
204
+ )
205
+ objects = DefaultTradeManager()
206
+ valid_customer_trade_objects = ValidCustomerTradeManager()
207
+ valid_external_customer_trade_objects = ValidCustomerTradeManager(without_internal_trade=True)
208
+
209
+ @transition(
210
+ field=status,
211
+ source=Status.DRAFT,
212
+ target=Status.SUBMIT,
213
+ permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
214
+ user.profile, portfolio=instance.portfolio
215
+ ),
216
+ custom={
217
+ "_transition_button": ActionButton(
218
+ method=RequestType.PATCH,
219
+ identifiers=("wbportfolio:trade",),
220
+ icon=WBIcon.SEND.icon,
221
+ key="submit",
222
+ label="Submit",
223
+ action_label="Submit",
224
+ # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
225
+ )
226
+ },
227
+ )
228
+ def submit(self, by=None, description=None, **kwargs):
229
+ pass
230
+
231
+ def can_submit(self):
232
+ pass
233
+
234
+ @transition(
235
+ field=status,
236
+ source=Status.SUBMIT,
237
+ target=Status.FAILED,
238
+ permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
239
+ user.profile, portfolio=instance.portfolio
240
+ ),
241
+ )
242
+ def fail(self, **kwargs):
243
+ pass
244
+
245
+ @cached_property
246
+ def underlying_instrument_price(self) -> InstrumentPrice | None:
247
+ try:
248
+ return self.underlying_instrument.valuations.get(date=self.transaction_date)
249
+ except InstrumentPrice.DoesNotExist:
250
+ return None
251
+
252
+ @transition(
253
+ field=status,
254
+ source=Status.SUBMIT,
255
+ target=GET_STATE(
256
+ lambda self, **kwargs: (
257
+ self.Status.EXECUTED if self.underlying_instrument_price is not None else self.Status.FAILED
258
+ ),
259
+ states=[Status.EXECUTED, Status.FAILED],
260
+ ),
261
+ permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
262
+ user.profile, portfolio=instance.portfolio
263
+ ),
264
+ custom={
265
+ "_transition_button": ActionButton(
266
+ method=RequestType.PATCH,
267
+ identifiers=("wbportfolio:trade",),
268
+ icon=WBIcon.CONFIRM.icon,
269
+ key="execute",
270
+ label="Execute",
271
+ action_label="Execute",
272
+ # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
273
+ )
274
+ },
275
+ on_error="FAILED",
276
+ )
277
+ def execute(self, **kwargs):
278
+ if self.underlying_instrument_price:
279
+ asset, created = AssetPosition.unannotated_objects.update_or_create(
280
+ underlying_instrument=self.underlying_instrument,
281
+ portfolio=self.portfolio,
282
+ date=self.transaction_date,
283
+ is_estimated=False,
284
+ defaults={
285
+ "initial_currency_fx_rate": self.currency_fx_rate,
286
+ "weighting": self._target_weight,
287
+ "initial_price": self.underlying_instrument_price.net_value,
288
+ "initial_shares": None,
289
+ "underlying_instrument_price": self.underlying_instrument_price,
290
+ "asset_valuation_date": self.transaction_date,
291
+ "currency": self.currency,
292
+ },
293
+ )
294
+ asset.set_weighting(self._target_weight)
295
+ else:
296
+ self.comment = f"Trade failed because no price is found for {self.underlying_instrument.computed_str} on {self.transaction_date:%Y-%m-%d}"
297
+
298
+ def can_execute(self):
299
+ if not self.portfolio.is_manageable:
300
+ return {"portfolio": "The portfolio needs to be a model portfolio in order to execute this trade manually"}
301
+
302
+ @transition(
303
+ field=status,
304
+ source=Status.EXECUTED,
305
+ target=Status.CONFIRMED,
306
+ permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
307
+ user.profile, portfolio=instance.portfolio
308
+ ),
309
+ custom={
310
+ "_transition_button": ActionButton(
311
+ method=RequestType.PATCH,
312
+ identifiers=("wbportfolio:trade",),
313
+ icon=WBIcon.CONFIRM.icon,
314
+ key="confirm",
315
+ label="Confirm",
316
+ action_label="Confirme",
317
+ # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
318
+ )
319
+ },
320
+ )
321
+ def confirm(self, by=None, description=None, **kwargs):
322
+ pass
323
+
324
+ def can_confirm(self):
325
+ pass
326
+
327
+ @transition(
328
+ field=status,
329
+ source=Status.SUBMIT,
330
+ target=Status.DRAFT,
331
+ permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
332
+ user.profile, portfolio=instance.portfolio
333
+ ),
334
+ custom={
335
+ "_transition_button": ActionButton(
336
+ method=RequestType.PATCH,
337
+ identifiers=("wbportfolio:trade",),
338
+ icon=WBIcon.UNDO.icon,
339
+ key="backtodraft",
340
+ label="Back to Draft",
341
+ action_label="backtodraft",
342
+ # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
343
+ )
344
+ },
345
+ )
346
+ def backtodraft(self, **kwargs):
347
+ pass
348
+
349
+ @transition(
350
+ field=status,
351
+ source=Status.EXECUTED,
352
+ target=Status.DRAFT,
353
+ permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
354
+ user.profile, portfolio=instance.portfolio
355
+ ),
356
+ custom={
357
+ "_transition_button": ActionButton(
358
+ method=RequestType.PATCH,
359
+ identifiers=("wbportfolio:trade",),
360
+ icon=WBIcon.UNDO.icon,
361
+ key="reverte",
362
+ label="Revert",
363
+ action_label="revert",
364
+ # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
365
+ )
366
+ },
367
+ )
368
+ def revert(self, to_date=None, **kwargs):
369
+ with suppress(AssetPosition.DoesNotExist):
370
+ asset = AssetPosition.objects.get(
371
+ underlying_instrument=self.underlying_instrument,
372
+ portfolio=self.portfolio,
373
+ date=self.transaction_date,
374
+ is_estimated=False,
375
+ )
376
+ asset.set_weighting(asset.weighting - self.weighting)
377
+
378
+ @property
379
+ def product(self):
380
+ from wbportfolio.models.products import Product
381
+
382
+ try:
383
+ return Product.objects.get(id=self.underlying_instrument.id)
384
+ except Product.DoesNotExist:
385
+ return None
386
+
387
+ @cached_property
388
+ @admin.display(description="Last Effective Date")
389
+ def _last_effective_date(self) -> date:
390
+ if hasattr(self, "last_effective_date"):
391
+ return self.last_effective_date
392
+ elif (
393
+ assets := AssetPosition.objects.filter(
394
+ underlying_instrument=self.underlying_instrument,
395
+ date__lt=self.transaction_date,
396
+ portfolio=self.portfolio,
397
+ )
398
+ ).exists():
399
+ return assets.latest("date").date
400
+
401
+ @cached_property
402
+ @admin.display(description="Effective Weight")
403
+ def _effective_weight(self) -> Decimal:
404
+ return getattr(
405
+ self,
406
+ "effective_weight",
407
+ AssetPosition.objects.filter(
408
+ underlying_instrument=self.underlying_instrument,
409
+ date=self._last_effective_date,
410
+ portfolio=self.portfolio,
411
+ ).aggregate(s=Sum("weighting"))["s"]
412
+ or Decimal(0),
413
+ )
414
+
415
+ @cached_property
416
+ @admin.display(description="Effective Shares")
417
+ def _effective_shares(self) -> Decimal:
418
+ return getattr(
419
+ self,
420
+ "effective_shares",
421
+ AssetPosition.objects.filter(
422
+ underlying_instrument=self.underlying_instrument,
423
+ date=self.transaction_date,
424
+ portfolio=self.portfolio,
425
+ ).aggregate(s=Sum("shares"))["s"]
426
+ or Decimal(0),
427
+ )
428
+
429
+ @cached_property
430
+ @admin.display(description="Target Weight")
431
+ def _target_weight(self) -> Decimal:
432
+ return getattr(self, "target_weight", self._effective_weight + self.weighting)
433
+
434
+ @cached_property
435
+ @admin.display(description="Target Shares")
436
+ def _target_shares(self) -> Decimal:
437
+ return getattr(self, "target_shares", self._effective_shares * self.weighting)
438
+
439
+ @cached_property
440
+ @admin.display(description="Diff Claims")
441
+ def _diff_shares(self) -> Decimal:
442
+ if hasattr(self, "diff_shares"):
443
+ return self.diff_shares
444
+ return self.shares - self.claimed_shares
445
+
446
+ order_with_respect_to = "trade_proposal"
447
+
448
+ class Meta(OrderedModel.Meta):
449
+ verbose_name = "Trade"
450
+ verbose_name_plural = "Trades"
451
+ constraints = [
452
+ models.CheckConstraint(
453
+ check=models.Q(marked_as_internal=False)
454
+ | (
455
+ models.Q(marked_as_internal=True)
456
+ & models.Q(transaction_subtype__in=["REDEMPTION", "SUBSCRIPTION"])
457
+ ),
458
+ name="marked_as_internal_only_for_subred",
459
+ ),
460
+ models.CheckConstraint(
461
+ check=models.Q(internal_trade__isnull=True)
462
+ | (
463
+ models.Q(internal_trade__isnull=False)
464
+ & models.Q(transaction_subtype__in=["REDEMPTION", "SUBSCRIPTION"])
465
+ ),
466
+ name="internal_trade_set_only_for_subred",
467
+ ),
468
+ ]
469
+ # notification_email_template = "portfolio/email/trade_notification.html"
470
+
471
+ def save(self, *args, **kwargs):
472
+ if not self.custodian and self.bank:
473
+ self.custodian = Custodian.get_by_mapping(self.bank)
474
+ if self.price is None:
475
+ # we try to get the price if not provided directly from the underlying instrument
476
+ with suppress(InstrumentPrice.DoesNotExist):
477
+ self.price = self.underlying_instrument.valuations.get(date=self.transaction_date).net_value
478
+ if self.price is not None and self.price_gross is None:
479
+ self.price_gross = self.price
480
+
481
+ if self.price is not None and self.shares is not None and self.total_value is None:
482
+ self.total_value = self.price * self.shares
483
+
484
+ if self.price_gross is not None and self.shares is not None and self.total_value_gross is None:
485
+ self.total_value_gross = self.price_gross * self.shares
486
+
487
+ if self.trade_proposal:
488
+ self.portfolio = self.trade_proposal.portfolio
489
+ self.transaction_date = self.trade_proposal.trade_date
490
+ if effective_shares := self._effective_shares:
491
+ self.shares = effective_shares * self.weighting
492
+ self.transaction_type = Transaction.Type.TRADE
493
+
494
+ if self.transaction_subtype is None:
495
+ # if subtype not provided, we extract it automatically from the existing data.
496
+ if self.underlying_instrument.instrument_type.key == "product":
497
+ if self.shares is not None:
498
+ if self.shares > 0:
499
+ self.transaction_subtype = Trade.Type.SUBSCRIPTION
500
+ elif self.shares < 0:
501
+ self.transaction_subtype = Trade.Type.REDEMPTION
502
+ elif self.weighting is not None:
503
+ if self.weighting > 0:
504
+ self.transaction_subtype = Trade.Type.BUY
505
+ elif self.weighting < 0:
506
+ self.transaction_subtype = Trade.Type.SELL
507
+ else:
508
+ self.transaction_subtype = Trade.Type.REBALANCE
509
+ if self.id and hasattr(self, "claims"):
510
+ self.claimed_shares = self.trade.claims.filter(status="APPROVED").aggregate(s=Sum("shares"))[
511
+ "s"
512
+ ] or Decimal(0)
513
+ if self.trade_proposal and self.trade_proposal.status == "DRAFT":
514
+ self.status = self.Status.DRAFT
515
+ if self.internal_trade:
516
+ self.marked_as_internal = True
517
+ super().save(*args, **kwargs)
518
+
519
+ def get_transaction_subtype(self) -> str:
520
+ """
521
+ Return the expected transaction subtype based n
522
+
523
+ """
524
+
525
+ def delete(self, **kwargs):
526
+ pre_collection.send(sender=self.__class__, instance=self)
527
+ super().delete(**kwargs)
528
+
529
+ def __str__(self):
530
+ ticker = f"{self.underlying_instrument.ticker}:" if self.underlying_instrument.ticker else ""
531
+ return f"{ticker}{self.shares} ({self.bank})"
532
+
533
+ def _build_dto(self) -> TradeDTO:
534
+ """
535
+ Data Transfer Object
536
+ Returns:
537
+ DTO trade object
538
+ """
539
+ return TradeDTO(
540
+ id=self.id,
541
+ underlying_instrument=self.underlying_instrument.id,
542
+ effective_weight=self._effective_weight,
543
+ target_weight=self._target_weight,
544
+ instrument_type=self.underlying_instrument.security_instrument_type,
545
+ currency=self.underlying_instrument.currency,
546
+ date=self.transaction_date,
547
+ )
548
+
549
+ def get_alternative_valid_trades(self, share_delta: float = 0):
550
+ return Trade.objects.filter(
551
+ Q(underlying_instrument=self.underlying_instrument)
552
+ & Q(portfolio=self.portfolio)
553
+ & (
554
+ Q(transaction_date__gte=self.transaction_date - timedelta(days=self.TRADE_WINDOW_INTERVAL))
555
+ & Q(transaction_date__lte=self.transaction_date + timedelta(days=self.TRADE_WINDOW_INTERVAL))
556
+ )
557
+ & Q(transaction_subtype=self.transaction_subtype)
558
+ & Q(shares__gte=self.shares * Decimal(1 - share_delta))
559
+ & Q(shares__lte=self.shares * Decimal(1 + share_delta))
560
+ & Q(marked_for_deletion=False)
561
+ & Q(claims__isnull=True)
562
+ & Q(pending=False)
563
+ ).exclude(id=self.id)
564
+
565
+ @property
566
+ def is_claimable(self) -> bool:
567
+ return self.is_customer_trade and not self.marked_for_deletion and not self.pending
568
+
569
+ @property
570
+ def is_customer_trade(self) -> bool:
571
+ return self.transaction_subtype in [Trade.Type.REDEMPTION.name, Trade.Type.SUBSCRIPTION.name]
572
+
573
+ @classmethod
574
+ def subquery_shares_per_underlying_instrument(
575
+ cls, val_date, underlying_instrument_name="pk", only_customer_trade=True
576
+ ):
577
+ """Returns a Subquery that returns the shares at a certain price date
578
+ or 0
579
+
580
+ Arguments:
581
+ val_date {datetime.date} -- The date that is used to determine which tradesare filtered
582
+
583
+ Keyword Arguments:
584
+ underlying_instrument_name {str} -- The reference to the underlying_instrument pk of the outer query (default: {"pk"})
585
+
586
+ Returns:
587
+ django.db.models.Subquery -- Subquery containing the sum of shares of each underlying_instrument
588
+ """
589
+
590
+ qs = cls.valid_customer_trade_objects
591
+ if not only_customer_trade:
592
+ qs = cls.objects
593
+ qs = qs.filter(
594
+ underlying_instrument=OuterRef(underlying_instrument_name),
595
+ transaction_date__lt=val_date,
596
+ )
597
+ return Coalesce(
598
+ Subquery(
599
+ qs.values("underlying_instrument").annotate(sum_shares=Sum("shares")).values("sum_shares")[:1],
600
+ output_field=models.DecimalField(),
601
+ ),
602
+ Decimal(0),
603
+ )
604
+
605
+ def link_to_internal_trade(self):
606
+ qs = Trade.objects.filter(
607
+ Q(underlying_instrument__instrument_type__key="product")
608
+ & Q(shares=self.shares)
609
+ & Q(underlying_instrument=self.underlying_instrument)
610
+ & Q(transaction_date__gte=self.transaction_date - timedelta(days=self.TRADE_WINDOW_INTERVAL))
611
+ & Q(transaction_date__lte=self.transaction_date + timedelta(days=self.TRADE_WINDOW_INTERVAL))
612
+ ).exclude(id=self.id)
613
+ if self.transaction_subtype in [Trade.Type.REDEMPTION, Trade.Type.SUBSCRIPTION]:
614
+ qs = qs.exclude(transaction_subtype__in=[Trade.Type.REDEMPTION, Trade.Type.SUBSCRIPTION])
615
+ if qs.count() == 1:
616
+ self.internal_trade = qs.first()
617
+ self.save()
618
+ else:
619
+ qs = qs.filter(transaction_subtype__in=[Trade.Type.REDEMPTION, Trade.Type.SUBSCRIPTION])
620
+ if qs.count() == 1:
621
+ trade = qs.first()
622
+ trade.internal_trade = self
623
+ trade.save()
624
+
625
+ @classmethod
626
+ def subquery_net_money(
627
+ cls, date_gte=None, date_lte=None, underlying_instrument_name="pk", only_positive=False, only_negative=False
628
+ ):
629
+ """Return a subquery which computes the net negative/positive money per underlying_instrument
630
+
631
+ Arguments:
632
+ val_date1 {datetime.date} -- The start date, including
633
+ val_date2 {datetime.date} -- The end date, including
634
+
635
+ Keyword Arguments:
636
+ underlying_instrument_name {str} -- The reference to the underlying_instrument pk from the outer query (default: {"pk"})
637
+
638
+ Returns:
639
+ django.db.models.Subquery -- The subquery containing the net negative money per underlying_instrument
640
+ """
641
+ qs = cls.valid_external_customer_trade_objects.annotate(
642
+ date_considered=ExpressionWrapper(F("transaction_date") + 1, output_field=DateField())
643
+ )
644
+
645
+ if date_gte:
646
+ qs = qs.filter(date_considered__gte=date_gte)
647
+ if date_lte:
648
+ qs = qs.filter(date_considered__lte=date_lte)
649
+
650
+ if only_positive:
651
+ qs = qs.filter(shares__gt=0)
652
+ elif only_negative:
653
+ qs = qs.filter(shares__lt=0)
654
+ return Coalesce(
655
+ Subquery(
656
+ qs.filter(underlying_instrument=OuterRef(underlying_instrument_name))
657
+ .annotate(
658
+ _price=Case(
659
+ When(
660
+ price__isnull=True,
661
+ then=InstrumentPrice.subquery_closest_value(
662
+ "net_value",
663
+ date_name="date_considered",
664
+ instrument_pk_name="underlying_instrument__pk",
665
+ ),
666
+ ),
667
+ default=F("price"),
668
+ ),
669
+ net_value=ExpressionWrapper(F("shares") * F("_price"), output_field=models.FloatField()),
670
+ )
671
+ .values("underlying_instrument")
672
+ .annotate(sum_net_value=Sum(F("net_value")))
673
+ .values("sum_net_value"),
674
+ output_field=models.FloatField(),
675
+ ),
676
+ 0.0,
677
+ )
678
+
679
+ @classmethod
680
+ def get_endpoint_basename(cls):
681
+ return "wbportfolio:trade"
682
+
683
+ @classmethod
684
+ def get_representation_endpoint(cls):
685
+ return "wbportfolio:traderepresentation-list"
686
+
687
+ @classmethod
688
+ def get_representation_label_key(cls):
689
+ return "{{|:-}}{{transaction_date}}{{|::}}{{bank}}{{|-:}} {{claimed_shares}} / {{shares}} (∆ {{diff_shares}})"
690
+
691
+
692
+ @shared_task
693
+ def align_custodian():
694
+ unaligned_qs = Trade.objects.annotate(
695
+ proper_custodian_id=Subquery(Custodian.objects.filter(mapping__contains=OuterRef("bank")).values("id")[:1])
696
+ ).exclude(custodian__id=F("proper_custodian_id"))
697
+
698
+ unaligned_qs.update(custodian__id=F("proper_custodian_id"))
699
+
700
+
701
+ @receiver(post_save, sender="wbportfolio.Claim")
702
+ def compute_claimed_shares_on_claim_save(sender, instance, created, raw, **kwargs):
703
+ if not raw and instance.trade:
704
+ instance.trade.save()