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,153 @@
1
+ import importlib
2
+ from contextlib import suppress
3
+
4
+ from celery import shared_task
5
+ from django.db import models
6
+ from django.db.models import Exists, OuterRef, Q, QuerySet
7
+ from django.dispatch import receiver
8
+ from wbfdm.models.instruments.instrument_prices import InstrumentPrice
9
+ from wbportfolio.import_export.handlers.fees import FeesImportHandler
10
+ from wbportfolio.models.products import Product
11
+
12
+ from .transactions import Transaction
13
+
14
+
15
+ class ValidFeesQueryset(QuerySet):
16
+ def filter_only_valid_fees(self) -> QuerySet:
17
+ """
18
+ Filter the queryset to remove duplicate in case calculated and non-calculated fees are present for the same date/product/type
19
+ """
20
+ return self.annotate(
21
+ real_fees_exists=Exists(
22
+ self.filter(
23
+ transaction_subtype=OuterRef("transaction_subtype"),
24
+ linked_product=OuterRef("linked_product"),
25
+ fee_date=OuterRef("fee_date"),
26
+ calculated=False,
27
+ )
28
+ )
29
+ ).filter(Q(calculated=False) | (Q(real_fees_exists=False) & Q(calculated=True)))
30
+
31
+
32
+ class DefaultFeesManager(models.Manager):
33
+ def get_queryset(self) -> ValidFeesQueryset:
34
+ return ValidFeesQueryset(self.model)
35
+
36
+ def filter_only_valid_fees(self) -> QuerySet:
37
+ return self.get_queryset().filter_only_valid_fees()
38
+
39
+
40
+ class ValidFeesManager(DefaultFeesManager):
41
+ def get_queryset(self) -> QuerySet:
42
+ return super().get_queryset().filter_only_valid_fees()
43
+
44
+
45
+ class Fees(Transaction):
46
+ import_export_handler_class = FeesImportHandler
47
+
48
+ class Type(models.TextChoices):
49
+ TRANSACTION = "TRANSACTION", "Transaction"
50
+ PERFORMANCE_CRYSTALIZED = "PERFORMANCE_CRYSTALIZED", "Performance Crystalized"
51
+ PERFORMANCE = "PERFORMANCE", "Performance"
52
+ MANAGEMENT = "MANAGEMENT", "Management"
53
+ ISSUER = "ISSUER", "Issuer"
54
+ OTHER = "OTHER", "Other"
55
+
56
+ transaction_subtype = models.CharField(
57
+ max_length=255, verbose_name="Fees Type", choices=Type.choices, default=Type.MANAGEMENT
58
+ )
59
+
60
+ fee_date = models.DateField() # needed for indexing
61
+ calculated = models.BooleanField(
62
+ default=True,
63
+ help_text="A marker whether the fees were calculated or supplied.",
64
+ verbose_name="Is calculated",
65
+ )
66
+
67
+ linked_product = models.ForeignKey(
68
+ "wbportfolio.Product",
69
+ related_name="transactionfees",
70
+ on_delete=models.PROTECT,
71
+ verbose_name="Product",
72
+ )
73
+
74
+ class Meta:
75
+ verbose_name = "Fees"
76
+ verbose_name_plural = "Fees"
77
+ indexes = [
78
+ models.Index(fields=["linked_product"]),
79
+ models.Index(fields=["transaction_subtype", "linked_product", "fee_date", "calculated"]),
80
+ ]
81
+ constraints = [
82
+ models.UniqueConstraint(
83
+ fields=["linked_product", "fee_date", "transaction_subtype", "calculated"], name="unique_fees"
84
+ ),
85
+ ]
86
+
87
+ objects = DefaultFeesManager()
88
+ valid_objects = ValidFeesManager()
89
+
90
+ def save(self, *args, **kwargs):
91
+ self.fee_date = self.transaction_date
92
+ super().save(*args, **kwargs)
93
+
94
+ def __str__(self):
95
+ return f"{self.transaction_date:%d.%m.%Y} - {self.Type[self.transaction_subtype]}: {self.portfolio.name}"
96
+
97
+ @classmethod
98
+ def get_endpoint_basename(cls):
99
+ return "wbportfolio:fees"
100
+
101
+
102
+ class FeeCalculation(models.Model):
103
+ name = models.CharField(max_length=128, verbose_name="Name")
104
+ import_path = models.CharField(max_length=512, verbose_name="Import Path", default="restbench.fees.default")
105
+
106
+ @classmethod
107
+ def compute_fee_from_price(cls, price):
108
+ product = Product.objects.get(id=price.instrument.id)
109
+ if (fee_calculation := product.fee_calculation) and (import_path := fee_calculation.import_path):
110
+ calculation_module = importlib.import_module(import_path)
111
+ for new_fees in calculation_module.fees_calculation(price.id):
112
+ Fees.objects.update_or_create(
113
+ linked_product=new_fees.pop("linked_product"),
114
+ transaction_date=new_fees.pop("transaction_date"),
115
+ transaction_subtype=new_fees.pop("transaction_subtype"),
116
+ calculated=True,
117
+ defaults=new_fees,
118
+ )
119
+
120
+ def __str__(self) -> str:
121
+ return self.name
122
+
123
+
124
+ @shared_task
125
+ def compute_fee_from_price_as_task(price_id):
126
+ price = InstrumentPrice.objects.get(id=price_id)
127
+ FeeCalculation.compute_fee_from_price(price)
128
+
129
+
130
+ @receiver(models.signals.post_save, sender="wbfdm.InstrumentPrice")
131
+ def update_or_create_fees_post(sender, instance, created, raw, **kwargs):
132
+ """Gets or create the fees for a given price and updates them if necessary"""
133
+ if not raw and created and not instance.calculated and instance.instrument:
134
+ with suppress(Product.DoesNotExist):
135
+ product = Product.objects.get(id=instance.instrument.id)
136
+ if product.fee_calculation:
137
+ compute_fee_from_price_as_task.delay(instance.id)
138
+
139
+
140
+ # @receiver(models.signals.pre_save, sender="wbportfolio.Fees")
141
+ # def check_uniqueness(sender, instance, raw, **kwargs):
142
+ # if (
143
+ # Fees.objects.exclude(id=instance.id)
144
+ # .filter(
145
+ # transaction_date=instance.transaction_date,
146
+ # transaction_subtype=instance.transaction_subtype,
147
+ # linked_product=instance.linked_product,
148
+ # )
149
+ # .exists()
150
+ # ):
151
+ # raise ValueError(
152
+ # f"A fees object already exists with date, type and product = {instance.transaction_date}, {instance.transaction_subtype}, {instance.linked_product}"
153
+ # )
@@ -0,0 +1,502 @@
1
+ from contextlib import suppress
2
+ from datetime import timedelta
3
+ from typing import TypeVar
4
+
5
+ import pandas as pd
6
+ from celery import shared_task
7
+ from django.core.exceptions import ValidationError
8
+ from django.db import models
9
+ from django.db.models.signals import post_save
10
+ from django.dispatch import receiver
11
+ from django.utils import timezone
12
+ from django.utils.functional import cached_property
13
+ from django_fsm import FSMField, transition
14
+ from pandas.tseries.offsets import BDay
15
+ from wbcompliance.models.risk_management.mixins import RiskCheckMixin
16
+ from wbcore.contrib.icons import WBIcon
17
+ from wbcore.enums import RequestType
18
+ from wbcore.metadata.configs.buttons import ActionButton
19
+ from wbcore.models import WBModel
20
+ from wbfdm.models.instruments.instruments import Instrument
21
+ from wbportfolio.models.roles import PortfolioRole
22
+ from wbportfolio.pms.trading import TradingService
23
+ from wbportfolio.pms.typing import Portfolio as PortfolioDTO
24
+ from wbportfolio.pms.typing import TradeBatch as TradeBatchDTO
25
+
26
+ from .trades import Trade
27
+
28
+ SelfTradeProposal = TypeVar("SelfTradeProposal", bound="TradeProposal")
29
+
30
+
31
+ class TradeProposal(RiskCheckMixin, WBModel):
32
+ trade_date = models.DateField(verbose_name="Trading Date")
33
+
34
+ class Status(models.TextChoices):
35
+ DRAFT = "DRAFT", "Draft"
36
+ SUBMIT = "SUBMIT", "Submit"
37
+ APPROVED = "APPROVED", "Approved"
38
+ DENIED = "DENIED", "Denied"
39
+
40
+ comment = models.TextField(default="", verbose_name="Trade Comment", blank=True)
41
+ status = FSMField(default=Status.DRAFT, choices=Status.choices, verbose_name="Status")
42
+ model_portfolio = models.ForeignKey(
43
+ "wbportfolio.Portfolio",
44
+ blank=True,
45
+ null=True,
46
+ related_name="model_trade_proposals",
47
+ on_delete=models.PROTECT,
48
+ verbose_name="Model Portfolio",
49
+ )
50
+ portfolio = models.ForeignKey(
51
+ "wbportfolio.Portfolio", related_name="trade_proposals", on_delete=models.PROTECT, verbose_name="Portfolio"
52
+ )
53
+ creator = models.ForeignKey(
54
+ "directory.Person",
55
+ blank=True,
56
+ null=True,
57
+ related_name="trade_proposals",
58
+ on_delete=models.PROTECT,
59
+ verbose_name="Owner",
60
+ )
61
+
62
+ def _get_checked_object_field_name(self) -> str:
63
+ """
64
+ Mandatory function from the Riskcheck mixin that returns the field (aka portfolio), representing the object to check the rules against.
65
+ """
66
+ return "portfolio"
67
+
68
+ @cached_property
69
+ def validated_trading_service(self) -> TradingService:
70
+ """
71
+ This property holds the validated trading services and cache it.This property expect to be set only if is_valid return True
72
+ """
73
+ return TradingService(
74
+ self.trade_date,
75
+ effective_portfolio=self.portfolio._build_dto(self.trade_date),
76
+ trades_batch=self._build_dto(),
77
+ )
78
+
79
+ @property
80
+ def previous_trade_proposal(self) -> SelfTradeProposal | None:
81
+ future_proposals = TradeProposal.objects.filter(portfolio=self.portfolio).filter(
82
+ trade_date__lt=self.trade_date, status=TradeProposal.Status.APPROVED
83
+ )
84
+ if future_proposals.exists():
85
+ return future_proposals.latest("trade_date")
86
+ return None
87
+
88
+ @property
89
+ def next_trade_proposal(self) -> SelfTradeProposal | None:
90
+ future_proposals = TradeProposal.objects.filter(portfolio=self.portfolio).filter(
91
+ trade_date__gt=self.trade_date, status=TradeProposal.Status.APPROVED
92
+ )
93
+ if future_proposals.exists():
94
+ return future_proposals.earliest("trade_date")
95
+ return None
96
+
97
+ @cached_property
98
+ def base_assets(self):
99
+ """
100
+ Return a dictionary representation (instrument_id: target weight) of this trade proposal
101
+ Returns:
102
+ A dictionary representation
103
+
104
+ """
105
+ return {
106
+ v["underlying_instrument"]: v["target_weight"]
107
+ for v in self.trades.filter(status=Trade.Status.EXECUTED).values("underlying_instrument", "target_weight")
108
+ }
109
+
110
+ def __str__(self) -> str:
111
+ return f"{self.portfolio.name}: {self.trade_date} ({self.status})"
112
+
113
+ def save(self, *args, **kwargs):
114
+ if not self.model_portfolio:
115
+ self.model_portfolio = self.portfolio
116
+ super().save(*args, **kwargs)
117
+
118
+ def _build_dto(self) -> TradeBatchDTO:
119
+ """
120
+ Data Transfer Object
121
+ Returns:
122
+ DTO trade object
123
+ """
124
+ return (
125
+ TradeBatchDTO(tuple([trade._build_dto() for trade in self.trades.all()])) if self.trades.exists() else None
126
+ )
127
+
128
+ # Start tools methods
129
+ def clone(self, **kwargs) -> SelfTradeProposal:
130
+ """
131
+ Method to clone self as a new trade proposal. It will automatically shift the trade date if a proposal already exists
132
+ Args:
133
+ **kwargs: The keyword arguments
134
+ Returns:
135
+ The cloned trade proposal
136
+ """
137
+ trade_date = kwargs.get("trade_date", self.trade_date)
138
+
139
+ # Find the next valid trade date
140
+ while TradeProposal.objects.filter(portfolio=self.portfolio, trade_date=trade_date).exists():
141
+ trade_date += timedelta(days=1)
142
+
143
+ trade_proposal_clone = TradeProposal.objects.create(
144
+ trade_date=trade_date,
145
+ comment=kwargs.get("comment", self.comment),
146
+ status=TradeProposal.Status.DRAFT,
147
+ model_portfolio=self.model_portfolio,
148
+ portfolio=self.portfolio,
149
+ creator=self.creator,
150
+ )
151
+
152
+ # For all existing trades, copy them to the new trade proposal
153
+ for trade in self.trades.all():
154
+ trade.pk = None
155
+ trade.trade_proposal = trade_proposal_clone
156
+ trade.transaction_date = trade_proposal_clone.trade_date
157
+ trade.save()
158
+ return trade_proposal_clone
159
+
160
+ def normalize_trades(self):
161
+ """
162
+ Call the trading service with the existing trades and normalize them in order to obtain a total sum target weight of 100%
163
+ The existing trade will be modified directly with the given normalization factor
164
+ """
165
+ service = TradingService(self.trade_date, trades_batch=self._build_dto())
166
+ service.normalize()
167
+ leftovers_trades = self.trades.all()
168
+ for _, trade in service.trades_batch.trades_map.items():
169
+ with suppress(Trade.DoesNotExist):
170
+ self.trades.update_or_create(
171
+ id=trade.id,
172
+ defaults={
173
+ "weighting": trade.delta_weight,
174
+ "shares": trade.target_shares,
175
+ },
176
+ )
177
+ leftovers_trades = leftovers_trades.exclude(id=trade.id)
178
+ leftovers_trades.delete()
179
+
180
+ def reset_trades(self):
181
+ """
182
+ Will delete all existing trades and recreate them from the method `create_or_update_trades`
183
+ """
184
+ # delete all existing trades
185
+ self.trades.all().delete()
186
+ # recreate them from scratch (if the portfolio has positions)
187
+ self.create_or_update_trades()
188
+
189
+ def apply_trades(self):
190
+ # We validate trade which will create or update the initial asset positions
191
+ self.trades.exclude(status=Trade.Status.SUBMIT).update(status=Trade.Status.SUBMIT)
192
+ for trade in self.trades.all():
193
+ trade.execute()
194
+ trade.save()
195
+ # We propagate the new portfolio composition until the next trade proposal or today if it doesn't exist yet
196
+ to_date = self.next_trade_proposal.trade_date if self.next_trade_proposal else timezone.now().date()
197
+
198
+ for from_date in pd.date_range(self.trade_date, to_date - timedelta(days=1), freq="B"):
199
+ to_date = (from_date + BDay(1)).date()
200
+ self.portfolio.propagate_or_update_assets(
201
+ from_date.date(),
202
+ to_date,
203
+ forward_price=False,
204
+ base_assets=self.base_assets,
205
+ delete_existing_assets=True,
206
+ )
207
+ self.portfolio.change_at_date(to_date, base_assets=self.base_assets, force_recompute_weighting=True)
208
+
209
+ def revert_trades(self):
210
+ self.trades.exclude(status=Trade.Status.EXECUTED).update(status=Trade.Status.EXECUTED)
211
+ for trade in self.trades.all():
212
+ trade.revert()
213
+ trade.save()
214
+ if previous_trade_proposal := self.previous_trade_proposal:
215
+ previous_trade_proposal.apply_trades()
216
+
217
+ def create_or_update_trades(
218
+ self, target_portfolio: PortfolioDTO = None, effective_portfolio: PortfolioDTO = None, reset: bool = False
219
+ ):
220
+ """
221
+ This function talk to the trading service layer in order to generate a list of valid trades to attach to the proposal
222
+
223
+ Args:
224
+ target_portfolio: The target portfolio that the trades needs to execute to. Absence of position means a sell
225
+ effective_portfolio: The current or effective portfolio to derivative effective weight from. Absence of position means a buy
226
+ reset: If true, delete the current attached trades
227
+ """
228
+ # if the target portfolio is not provided, we try to build it
229
+ if (
230
+ not target_portfolio
231
+ and (assets := self.model_portfolio.assets.filter(date__lte=self.trade_date)).exists()
232
+ and (latest_pos := assets.latest("date"))
233
+ ):
234
+ target_portfolio = self.model_portfolio._build_dto(latest_pos.date)
235
+
236
+ # if the effective portfolio is not provided, we try to build it
237
+ if (
238
+ not effective_portfolio
239
+ and (assets := self.portfolio.assets.filter(date__lte=self.trade_date)).exists()
240
+ and (latest_pos := assets.latest("date"))
241
+ ):
242
+ effective_portfolio = self.portfolio._build_dto(latest_pos.date)
243
+ # Build trades DTO from the attached trades
244
+ trade_batch = self._build_dto()
245
+ if target_portfolio or effective_portfolio or trade_batch:
246
+ service = TradingService(
247
+ self.trade_date,
248
+ effective_portfolio=effective_portfolio,
249
+ target_portfolio=target_portfolio,
250
+ trades_batch=trade_batch,
251
+ )
252
+ # with suppress(ValidationError):
253
+ # Normalize the trades and validate it
254
+ service.normalize()
255
+ service.is_valid()
256
+ if reset:
257
+ self.trades.all().delete()
258
+ for trade_dto in service.validated_trades:
259
+ instrument = Instrument.objects.get(id=trade_dto.underlying_instrument)
260
+ t, c = Trade.objects.update_or_create(
261
+ underlying_instrument=instrument,
262
+ currency=instrument.currency,
263
+ transaction_date=self.trade_date,
264
+ trade_proposal=self,
265
+ portfolio=self.portfolio,
266
+ defaults={
267
+ "shares": trade_dto.target_shares,
268
+ "weighting": trade_dto.delta_weight,
269
+ "status": Trade.Status.DRAFT,
270
+ "currency_fx_rate": instrument.currency.convert(self.trade_date, self.portfolio.currency),
271
+ },
272
+ )
273
+
274
+ # End tools methods
275
+
276
+ # Start FSM logics
277
+
278
+ @transition(
279
+ field=status,
280
+ source=Status.DRAFT,
281
+ target=Status.SUBMIT,
282
+ permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
283
+ user.profile, portfolio=instance.portfolio
284
+ ),
285
+ custom={
286
+ "_transition_button": ActionButton(
287
+ method=RequestType.PATCH,
288
+ identifiers=("wbportfolio:tradeproposal",),
289
+ icon=WBIcon.SEND.icon,
290
+ key="submit",
291
+ label="Submit",
292
+ action_label="Submit",
293
+ # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
294
+ )
295
+ },
296
+ )
297
+ def submit(self, by=None, description=None, **kwargs):
298
+ self.trades.update(status=Trade.Status.SUBMIT)
299
+ self.evaluate_active_rules(
300
+ self.trade_date, self.validated_trading_service.target_portfolio, asynchronously=True
301
+ )
302
+
303
+ def can_submit(self):
304
+ errors = dict()
305
+ errors_list = []
306
+ if self.trades.exists() and self.trades.exclude(status=Trade.Status.DRAFT).exists():
307
+ errors_list.append("All trades need to be draft before submitting")
308
+ service = self.validated_trading_service
309
+ try:
310
+ service.is_valid(ignore_error=True)
311
+ # if service.trades_batch.totat_abs_delta_weight == 0:
312
+ # errors_list.append(
313
+ # "There is no change detected in this trade proposal. Please submit at last one valid trade"
314
+ # )
315
+ if len(service.validated_trades) == 0:
316
+ errors_list.append("There is no valid trade on this proposal")
317
+ if service.errors:
318
+ errors_list.extend(service.errors)
319
+ if errors_list:
320
+ errors["non_field_errors"] = errors_list
321
+ except ValidationError:
322
+ errors["non_field_errors"] = service.errors
323
+ with suppress(KeyError):
324
+ del self.__dict__["validated_trading_service"]
325
+ return errors
326
+
327
+ @property
328
+ def can_be_approved_or_denied(self):
329
+ return self.has_no_rule_or_all_checked_succeed and self.portfolio.is_manageable
330
+
331
+ @transition(
332
+ field=status,
333
+ source=Status.SUBMIT,
334
+ target=Status.APPROVED,
335
+ permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
336
+ user.profile, portfolio=instance.portfolio
337
+ )
338
+ and instance.can_be_approved_or_denied,
339
+ custom={
340
+ "_transition_button": ActionButton(
341
+ method=RequestType.PATCH,
342
+ identifiers=("wbportfolio:tradeproposal",),
343
+ icon=WBIcon.APPROVE.icon,
344
+ key="approve",
345
+ label="Approve",
346
+ action_label="Approve",
347
+ # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
348
+ )
349
+ },
350
+ )
351
+ def approve(self, by=None, description=None, **kwargs):
352
+ apply_trades_proposal_as_task.delay(self.id)
353
+
354
+ def can_approve(self):
355
+ errors = dict()
356
+ if self.trades.exclude(status=Trade.Status.SUBMIT).exists():
357
+ errors["non_field_errors"] = "At least one trade needs to be submitted to be able to approve this proposal"
358
+ if not self.portfolio.is_manageable:
359
+ errors[
360
+ "portfolio"
361
+ ] = "The portfolio needs to be a model portfolio in order to approve this trade proposal manually"
362
+ if self.has_assigned_active_rules and not self.has_all_check_completed_and_succeed:
363
+ errors["non_field_errors"] = "The pre trades rules did not passed successfully"
364
+ return errors
365
+
366
+ @transition(
367
+ field=status,
368
+ source=Status.SUBMIT,
369
+ target=Status.DENIED,
370
+ permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
371
+ user.profile, portfolio=instance.portfolio
372
+ )
373
+ and instance.can_be_approved_or_denied,
374
+ custom={
375
+ "_transition_button": ActionButton(
376
+ method=RequestType.PATCH,
377
+ identifiers=("wbportfolio:tradeproposal",),
378
+ icon=WBIcon.DENY.icon,
379
+ key="deny",
380
+ label="Deny",
381
+ action_label="Deny",
382
+ # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
383
+ )
384
+ },
385
+ )
386
+ def deny(self, by=None, description=None, **kwargs):
387
+ self.trades.all().delete()
388
+ with suppress(KeyError):
389
+ del self.__dict__["validated_trading_service"]
390
+
391
+ def can_deny(self):
392
+ errors = dict()
393
+ if self.trades.exclude(status=Trade.Status.SUBMIT).exists():
394
+ errors["non_field_errors"] = "At least one trade needs to be submitted to be able to deny this proposal"
395
+ return errors
396
+
397
+ @transition(
398
+ field=status,
399
+ source=Status.SUBMIT,
400
+ target=Status.DRAFT,
401
+ permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
402
+ user.profile, portfolio=instance.portfolio
403
+ )
404
+ and instance.has_all_check_completed, # we wait for all checks to succeed before proposing the back to draft transition
405
+ custom={
406
+ "_transition_button": ActionButton(
407
+ method=RequestType.PATCH,
408
+ identifiers=("wbportfolio:tradeproposal",),
409
+ icon=WBIcon.UNDO.icon,
410
+ key="backtodraft",
411
+ label="Back to Draft",
412
+ action_label="backtodraft",
413
+ # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
414
+ )
415
+ },
416
+ )
417
+ def backtodraft(self, **kwargs):
418
+ with suppress(KeyError):
419
+ del self.__dict__["validated_trading_service"]
420
+ self.trades.update(status=Trade.Status.DRAFT)
421
+ self.checks.delete()
422
+
423
+ def can_backtodraft(self):
424
+ errors = dict()
425
+ if self.trades.exclude(status=Trade.Status.SUBMIT).exists():
426
+ errors["non_field_errors"] = "All trades need to be submitted before reverting back to draft"
427
+ return errors
428
+
429
+ @transition(
430
+ field=status,
431
+ source=Status.APPROVED,
432
+ target=Status.DRAFT,
433
+ permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
434
+ user.profile, portfolio=instance.portfolio
435
+ ),
436
+ custom={
437
+ "_transition_button": ActionButton(
438
+ method=RequestType.PATCH,
439
+ identifiers=("wbportfolio:tradeproposal",),
440
+ icon=WBIcon.REGENERATE.icon,
441
+ key="revert",
442
+ label="Revert",
443
+ action_label="revert",
444
+ description_fields="<p>Unapply trades and move everything back to draft (i.e. The underlying asset positions will change like the trades were never applied)</p>",
445
+ )
446
+ },
447
+ )
448
+ def revert(self, **kwargs):
449
+ with suppress(KeyError):
450
+ del self.__dict__["validated_trading_service"]
451
+ revert_trade_proposal_as_task.delay(self.id, **kwargs)
452
+
453
+ def can_revert(self):
454
+ errors = dict()
455
+ if self.trades.exclude(status=Trade.Status.EXECUTED).exists():
456
+ errors["non_field_errors"] = "All trades need to be executed before reverting"
457
+ if not self.portfolio.is_manageable:
458
+ errors[
459
+ "portfolio"
460
+ ] = "The portfolio needs to be a model portfolio in order to revert this trade proposal manually"
461
+ return errors
462
+
463
+ # End FSM logics
464
+
465
+ @classmethod
466
+ def get_endpoint_basename(cls) -> str:
467
+ return "wbportfolio:tradeproposal"
468
+
469
+ @classmethod
470
+ def get_representation_endpoint(cls) -> str:
471
+ return "wbportfolio:tradeproposalrepresentation-list"
472
+
473
+ @classmethod
474
+ def get_representation_value_key(cls) -> str:
475
+ return "id"
476
+
477
+ @classmethod
478
+ def get_representation_label_key(cls) -> str:
479
+ return "{{_portfolio.name}} ({{trade_date}})"
480
+
481
+ class Meta:
482
+ verbose_name = "Trade Proposal"
483
+ verbose_name_plural = "Trade Proposals"
484
+ unique_together = ["portfolio", "trade_date"]
485
+
486
+
487
+ @shared_task(queue="portfolio")
488
+ def apply_trades_proposal_as_task(trade_proposal_id):
489
+ trade_proposal = TradeProposal.objects.get(id=trade_proposal_id)
490
+ trade_proposal.apply_trades()
491
+
492
+
493
+ @shared_task(queue="portfolio")
494
+ def revert_trade_proposal_as_task(trade_proposal_id, **kwargs):
495
+ trade_proposal = TradeProposal.objects.get(id=trade_proposal_id)
496
+ trade_proposal.revert_trades()
497
+
498
+
499
+ @receiver(post_save, sender="wbportfolio.TradeProposal")
500
+ def post_save_trade_proposal(sender, instance, created, raw, **kwargs):
501
+ if created and not raw and instance.portfolio.assets.filter(date__lte=instance.trade_date).exists():
502
+ instance.create_or_update_trades(reset=True)