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,933 @@
1
+ import json
2
+ from contextlib import suppress
3
+ from datetime import date, timedelta
4
+ from decimal import Decimal
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import plotly.graph_objects as go
9
+ from django.apps import apps
10
+ from django.contrib.messages import warning
11
+ from django.db.models import (
12
+ BooleanField,
13
+ Case,
14
+ CharField,
15
+ DecimalField,
16
+ ExpressionWrapper,
17
+ F,
18
+ FloatField,
19
+ OuterRef,
20
+ Q,
21
+ Subquery,
22
+ Sum,
23
+ Value,
24
+ When,
25
+ )
26
+ from django.db.models.functions import Coalesce, Concat, Least
27
+ from django.shortcuts import get_object_or_404
28
+ from django.utils.dateparse import parse_date
29
+ from django.utils.functional import cached_property
30
+ from rest_framework import filters, status, viewsets
31
+ from rest_framework.decorators import action
32
+ from rest_framework.response import Response
33
+ from wbcore import viewsets as wb_viewsets
34
+ from wbcore.contrib.currency.models import CurrencyFXRates
35
+ from wbcore.contrib.directory.models import Entry
36
+ from wbcore.contrib.io.viewsets import ExportPandasAPIViewSet
37
+ from wbcore.enums import WidgetType
38
+ from wbcore.filters import DjangoFilterBackend
39
+ from wbcore.pandas import fields as pf
40
+ from wbcore.serializers import decorator
41
+ from wbcore.utils.date import get_date_interval_from_request
42
+ from wbcore.utils.strings import format_number
43
+ from wbcrm.models.accounts import Account
44
+ from wbfdm.models import ClassificationGroup, InstrumentPrice
45
+ from wbfdm.preferences import get_default_classification_group
46
+ from wbportfolio.analysis.claims import ConsolidatedTradeSummary
47
+ from wbportfolio.filters import (
48
+ ClaimFilter,
49
+ ClaimGroupByFilter,
50
+ ConsolidatedTradeSummaryTableFilterSet,
51
+ CumulativeNNMChartFilter,
52
+ CustomerAPIFilter,
53
+ CustomerClaimFilter,
54
+ CustomerClaimGroupByFilter,
55
+ NegativeTermimalAccountPerProductFilterSet,
56
+ ProfitAndLossPandasFilter,
57
+ )
58
+ from wbportfolio.models import Product, Trade
59
+ from wbportfolio.models.transactions.claim import Claim, ClaimGroupbyChoice
60
+ from wbportfolio.preferences import get_monthly_nnm_target
61
+ from wbportfolio.serializers import (
62
+ ClaimAPIModelSerializer,
63
+ ClaimCustomerModelSerializer,
64
+ ClaimModelSerializer,
65
+ ClaimRepresentationSerializer,
66
+ ClaimTradeModelSerializer,
67
+ NegativeTermimalAccountPerProductModelSerializer,
68
+ )
69
+
70
+ from ..configs.buttons import (
71
+ ClaimTradeButtonConfig,
72
+ ConsolidatedTradeSummaryButtonConfig,
73
+ )
74
+ from ..configs.buttons.claims import TransferTradeSerializer
75
+ from ..configs.display import (
76
+ ClaimDisplayConfig,
77
+ ConsolidatedTradeSummaryDisplayConfig,
78
+ NegativeTermimalAccountPerProductDisplayConfig,
79
+ ProfitAndLossPandasDisplayConfig,
80
+ )
81
+ from ..configs.endpoints import (
82
+ ClaimAccountEndpointConfig,
83
+ ClaimEndpointConfig,
84
+ ClaimEntryEndpointConfig,
85
+ ClaimProductEndpointConfig,
86
+ ClaimTradeEndpointConfig,
87
+ ConsolidatedTradeSummaryDistributionChartEndpointConfig,
88
+ ConsolidatedTradeSummaryEndpointConfig,
89
+ CumulativeNNMChartEndpointConfig,
90
+ NegativeTermimalAccountPerProductEndpointConfig,
91
+ ProfitAndLossPandasEndpointConfig,
92
+ )
93
+ from ..configs.titles import (
94
+ ClaimAccountTitleConfig,
95
+ ClaimEntryTitleConfig,
96
+ ClaimProductTitleConfig,
97
+ ClaimTitleConfig,
98
+ ClaimTradeTitleConfig,
99
+ ConsolidatedTradeSummaryDistributionChartTitleConfig,
100
+ ConsolidatedTradeSummaryTitleConfig,
101
+ CumulativeNNMChartTitleConfig,
102
+ NegativeTermimalAccountPerProductTitleConfig,
103
+ ProfitAndLossPandasTitleConfig,
104
+ )
105
+ from .mixins import ClaimPermissionMixin
106
+
107
+
108
+ class ClaimAPIModelViewSet(ClaimPermissionMixin, viewsets.ModelViewSet):
109
+ filter_backends = [DjangoFilterBackend]
110
+ filterset_class = CustomerAPIFilter
111
+ queryset = Claim.objects.select_related("product", "claimant")
112
+ serializer_class = ClaimAPIModelSerializer
113
+
114
+
115
+ class ClaimRepresentationViewSet(ClaimPermissionMixin, wb_viewsets.RepresentationViewSet):
116
+ IDENTIFIER = "wbportfolio:claim"
117
+
118
+ filter_backends = (filters.OrderingFilter, filters.SearchFilter)
119
+ serializer_class = ClaimRepresentationSerializer
120
+ queryset = Claim.objects.all()
121
+
122
+ ordering_fields = ("title",)
123
+ search_fields = (
124
+ "product__name",
125
+ "product__isin",
126
+ "product__ticker",
127
+ "claimant__computed_str",
128
+ )
129
+ ordering = ["id"]
130
+
131
+ def get_queryset(self):
132
+ return (
133
+ super()
134
+ .get_queryset()
135
+ .exclude(status=Claim.Status.WITHDRAWN)
136
+ .select_related("product", "claimant", "account")
137
+ )
138
+
139
+
140
+ class ClaimModelViewSet(ClaimPermissionMixin, wb_viewsets.ModelViewSet):
141
+ IDENTIFIER = "wbportfolio:claim"
142
+
143
+ serializer_class = ClaimModelSerializer
144
+ queryset = Claim.objects.exclude(status=Claim.Status.WITHDRAWN)
145
+
146
+ search_fields = [
147
+ "claimant__computed_str",
148
+ "product__name",
149
+ "product__isin",
150
+ "bank",
151
+ "account__title",
152
+ ]
153
+ ordering_fields = ("date", "shares", "claimant__computed_str", "product__name", "account__title", "bank")
154
+ ordering = ["id", "-date"]
155
+
156
+ display_config_class = ClaimDisplayConfig
157
+ title_config_class = ClaimTitleConfig
158
+ endpoint_config_class = ClaimEndpointConfig
159
+
160
+ def get_filterset_class(self, request):
161
+ profile = request.user.profile
162
+ if profile.is_internal or request.user.is_superuser:
163
+ return ClaimFilter
164
+ return CustomerClaimFilter
165
+
166
+ def get_messages(self, request, queryset=None, paginated_queryset=None, instance=None, initial=False):
167
+ if instance and instance.product and instance.account:
168
+ sum_shares = Claim.objects.exclude(status=Claim.Status.WITHDRAWN).filter(
169
+ product=instance.product, account=instance.account
170
+ ).aggregate(s=Sum("shares"))["s"] or Decimal(0.0)
171
+ if sum_shares < 0:
172
+ warning(
173
+ request,
174
+ f"The total shares balance for this account and for this product is negative of {sum_shares}",
175
+ )
176
+
177
+ def get_aggregates(self, queryset, paginated_queryset):
178
+ return {
179
+ "shares": {"Σ": format_number(queryset.aggregate(s=Sum("shares"))["s"] or 0, decimal=4)},
180
+ }
181
+
182
+ def get_serializer_class(self):
183
+ profile = self.request.user.profile
184
+ if profile.is_internal or self.request.user.is_superuser:
185
+ return super().get_serializer_class()
186
+ return ClaimCustomerModelSerializer
187
+
188
+ def get_queryset(self):
189
+ return (
190
+ super()
191
+ .get_queryset()
192
+ .annotate(
193
+ currency=F("product__currency__symbol"),
194
+ last_nav=InstrumentPrice.subquery_closest_value(
195
+ "net_value", instrument_pk_name="product", date_lookup="exact"
196
+ ),
197
+ fx_rate=CurrencyFXRates.get_fx_rates_subquery("date", lookup_expr="exact"),
198
+ total_value=F("last_nav") * F("shares"),
199
+ total_value_usd=ExpressionWrapper(F("fx_rate") * F("total_value"), output_field=FloatField()),
200
+ trade_claimed_shares=Coalesce(
201
+ Subquery(
202
+ Claim.objects.filter(Q(status=Claim.Status.APPROVED) & Q(trade=OuterRef("trade")))
203
+ .values("trade")
204
+ .annotate(s=Sum("shares"))
205
+ .values("s")[:1]
206
+ ),
207
+ Decimal(0),
208
+ ),
209
+ quantity=Case(
210
+ When(as_shares=True, then=F("shares")),
211
+ When(as_shares=False, then=F("nominal_amount")),
212
+ default=None,
213
+ output_field=DecimalField(max_digits=15, decimal_places=2),
214
+ ),
215
+ trade_type=Case(
216
+ When(shares__gte=0, then=Value(True)),
217
+ When(shares__lt=0, then=Value(False)),
218
+ output_field=BooleanField(),
219
+ ),
220
+ trade_comment=F("trade__comment"),
221
+ )
222
+ .select_related(
223
+ "account",
224
+ "trade",
225
+ "product",
226
+ "creator",
227
+ "claimant",
228
+ )
229
+ .defer("account__reference_id")
230
+ )
231
+
232
+
233
+ class ClaimAccountModelViewSet(ClaimModelViewSet):
234
+ title_config_class = ClaimAccountTitleConfig
235
+ endpoint_config_class = ClaimAccountEndpointConfig
236
+
237
+ @cached_property
238
+ def account(self):
239
+ return get_object_or_404(Account, id=self.kwargs["account_id"])
240
+
241
+ def get_queryset(self):
242
+ return super().get_queryset().filter(account__in=self.account.get_descendants(include_self=True))
243
+
244
+
245
+ class ClaimProductModelViewSet(ClaimModelViewSet):
246
+ title_config_class = ClaimProductTitleConfig
247
+ endpoint_config_class = ClaimProductEndpointConfig
248
+
249
+ @cached_property
250
+ def product(self):
251
+ return get_object_or_404(Product, id=self.kwargs["product_id"])
252
+
253
+ def get_queryset(self):
254
+ return super().get_queryset().filter(product=self.product)
255
+
256
+ def get_identifier(self, request, identifier=None):
257
+ return "commission:product-claim"
258
+
259
+
260
+ class ClaimEntryModelViewSet(ClaimModelViewSet):
261
+ serializer_class = ClaimModelSerializer
262
+
263
+ display_config_class = ClaimDisplayConfig
264
+ title_config_class = ClaimEntryTitleConfig
265
+ endpoint_config_class = ClaimEntryEndpointConfig
266
+ ordering = ["id"]
267
+
268
+ def setup(self, request, *args, **kwargs):
269
+ super().setup(request, *args, **kwargs)
270
+ self.kwargs["claimant_id"] = self.kwargs["entry_id"] # ensure claimant_id is available for the serializer
271
+
272
+ @cached_property
273
+ def entry(self):
274
+ return Entry.objects.get(id=self.kwargs["entry_id"])
275
+
276
+ def get_queryset(self):
277
+ return super().get_queryset().filter_for_customer(self.entry)
278
+
279
+
280
+ class ClaimTradeModelViewSet(ClaimModelViewSet):
281
+ IDENTIFIER = "commission:trade-claim"
282
+
283
+ serializer_class = ClaimTradeModelSerializer
284
+
285
+ title_config_class = ClaimTradeTitleConfig
286
+ endpoint_config_class = ClaimTradeEndpointConfig
287
+ button_config_class = ClaimTradeButtonConfig
288
+
289
+ @cached_property
290
+ def trade(self) -> Trade:
291
+ return get_object_or_404(Trade, pk=self.kwargs["trade_id"])
292
+
293
+ @cached_property
294
+ def product(self) -> Product | None:
295
+ with suppress(Product.DoesNotExist):
296
+ return Product.objects.get(id=self.trade.underlying_instrument.id)
297
+
298
+ def get_queryset(self):
299
+ return super().get_queryset().filter(trade__id=self.kwargs["trade_id"])
300
+
301
+ @action(methods=["POST"], detail=False)
302
+ def transfer_trade(self, request, trade_id):
303
+ serializer = TransferTradeSerializer(data=request.data)
304
+ if serializer.is_valid():
305
+ trade = Trade.objects.get(id=trade_id)
306
+ transfer_date = (parse_date(serializer.data["transfer_date"]) - pd.tseries.offsets.BDay(0)).date()
307
+ from_account = serializer.data["from_account"]
308
+ to_account = serializer.data["to_account"]
309
+
310
+ if not transfer_date or not from_account or not to_account:
311
+ return Response(
312
+ {"__notification": {"title": "Trade not Transferred."}}, status=status.HTTP_400_BAD_REQUEST
313
+ )
314
+
315
+ Claim.objects.create(
316
+ trade=trade,
317
+ shares=trade.shares * -1,
318
+ bank=trade.bank,
319
+ date=transfer_date,
320
+ product=trade.product,
321
+ account_id=from_account,
322
+ claimant_id=request.user.profile.id,
323
+ )
324
+
325
+ Claim.objects.create(
326
+ trade=trade,
327
+ shares=trade.shares,
328
+ bank=trade.bank,
329
+ date=transfer_date,
330
+ product=trade.product,
331
+ account_id=to_account,
332
+ claimant_id=request.user.profile.id,
333
+ )
334
+
335
+ return Response({"__notification": {"title": "Trade Transferred."}}, status=status.HTTP_200_OK)
336
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
337
+
338
+ @action(methods=["POST"], detail=False)
339
+ def quick_claim(self, request, trade_id):
340
+ trade = Trade.objects.get(id=trade_id)
341
+
342
+ account = request.data.get("account")
343
+
344
+ if not account:
345
+ return Response({"__notification": {"title": "Trade not Claimed."}}, status=status.HTTP_400_BAD_REQUEST)
346
+
347
+ Claim.objects.create(
348
+ trade=trade,
349
+ shares=trade.shares,
350
+ bank=trade.bank,
351
+ date=trade.transaction_date,
352
+ product=trade.product,
353
+ account_id=account,
354
+ claimant_id=request.user.profile.id,
355
+ )
356
+
357
+ return Response({"__notification": {"title": "Trade Claimed."}}, status=status.HTTP_200_OK)
358
+
359
+
360
+ # Abstract AUM Viewset
361
+
362
+
363
+ class ConsolidatedTradeSummaryTableView(ClaimPermissionMixin, ExportPandasAPIViewSet):
364
+ IDENTIFIER = "commission:aum"
365
+
366
+ def get_pandas_fields(self, request):
367
+ fields = [
368
+ pf.PKField(key="id", label="ID"),
369
+ pf.CharField(key="title", label="Title"),
370
+ pf.DateField(key="initial_investment_date", label="First Investment"),
371
+ pf.SparklineField(key="aum_sparkline", label="AUM"),
372
+ pf.FloatField(key="sum_shares_start", label="Shares Start", precision=0),
373
+ pf.FloatField(key="sum_shares_end", label="Shares End", precision=0),
374
+ pf.FloatField(key="sum_shares_diff", label="Share difference", precision=0),
375
+ pf.FloatField(key="sum_shares_perf", label="Share Perf", precision=2, percent=True),
376
+ pf.FloatField(
377
+ key="sum_aum_start",
378
+ label="AUM Start",
379
+ precision=0,
380
+ decorators=(decorator(decorator_type="text", position="left", value="$"),),
381
+ ),
382
+ pf.FloatField(
383
+ key="sum_aum_end",
384
+ label="AUM End",
385
+ precision=0,
386
+ decorators=(decorator(decorator_type="text", position="left", value="$"),),
387
+ ),
388
+ pf.FloatField(
389
+ key="sum_aum_diff",
390
+ label="Nominal difference",
391
+ precision=0,
392
+ decorators=(decorator(decorator_type="text", position="left", value="$"),),
393
+ ),
394
+ pf.FloatField(key="sum_aum_perf", label="% difference", precision=2, percent=True),
395
+ pf.FloatField(
396
+ key="sum_nnm_total",
397
+ label="NNM",
398
+ precision=0,
399
+ decorators=(decorator(decorator_type="text", position="left", value="$"),),
400
+ ),
401
+ pf.FloatField(key="sum_nnm_perf", label="NNM (%)", precision=2, percent=True),
402
+ pf.FloatField(key="total_performance", label="Performance", precision=2),
403
+ pf.FloatField(key="total_performance_perf", label="Performance (%)", precision=2, percent=True),
404
+ ]
405
+ for nnm_column in self.nnm_monthly_columns:
406
+ fields.append(
407
+ pf.FloatField(
408
+ key=nnm_column[0],
409
+ label=nnm_column[1],
410
+ precision=0,
411
+ decorators=(decorator(decorator_type="text", position="left", value="$"),),
412
+ ),
413
+ )
414
+ if self.commission_type_columns:
415
+ fields.append(
416
+ pf.FloatField(
417
+ key="rebate_total",
418
+ label="Rebate",
419
+ precision=0,
420
+ decorators=(decorator(decorator_type="text", position="left", value="$"),),
421
+ )
422
+ )
423
+
424
+ for ct in self.commission_type_columns:
425
+ fields.append(
426
+ pf.FloatField(
427
+ key=ct[0],
428
+ label=ct[1],
429
+ precision=0,
430
+ decorators=(decorator(decorator_type="text", position="left", value="$"),),
431
+ )
432
+ )
433
+
434
+ return pf.PandasFields(fields=fields)
435
+
436
+ queryset = Claim.objects.filter(status=Claim.Status.APPROVED, account__isnull=False)
437
+
438
+ filterset_class = ConsolidatedTradeSummaryTableFilterSet
439
+
440
+ search_fields = ["title"]
441
+
442
+ def get_ordering_fields(self):
443
+ fields = [
444
+ "title",
445
+ "sum_shares_start",
446
+ "sum_shares_end",
447
+ "sum_shares_diff",
448
+ "sum_shares_perf",
449
+ "sum_aum_start",
450
+ "sum_aum_end",
451
+ "sum_aum_diff",
452
+ "sum_aum_perf",
453
+ "sum_nnm_total",
454
+ "sum_nnm_perf",
455
+ "total_performance",
456
+ "total_performance_perf",
457
+ *map(lambda x: x[0], self.nnm_monthly_columns),
458
+ ]
459
+ if self.commission_type_columns:
460
+ fields.extend(list(map(lambda x: x[0], self.commission_type_columns)))
461
+ fields.append("rebate_total")
462
+ return fields
463
+
464
+ ordering = ["-sum_aum_end"]
465
+ display_config_class = ConsolidatedTradeSummaryDisplayConfig
466
+ title_config_class = ConsolidatedTradeSummaryTitleConfig
467
+ endpoint_config_class = ConsolidatedTradeSummaryEndpointConfig
468
+ button_config_class = ConsolidatedTradeSummaryButtonConfig
469
+
470
+ @cached_property
471
+ def commission_type_columns(self) -> list[tuple[str, str]]:
472
+ if apps.is_installed("wbcommission"):
473
+ from wbcommission.models.commission import CommissionType
474
+
475
+ return list(map(lambda x: ("rebate_" + x[0], x[1]), CommissionType.objects.values_list("key", "name")))
476
+
477
+ @cached_property
478
+ def nnm_monthly_columns(self) -> list[tuple[str, str]]:
479
+ res = []
480
+ if self.start_date and self.end_date:
481
+ # + MonthEnd to ensure that we include the current month
482
+ for _d in pd.date_range(
483
+ self.start_date, self.end_date - timedelta(days=1) + pd.offsets.MonthEnd(), freq="ME"
484
+ ):
485
+ res.append(("sum_nnm_" + _d.strftime("%Y-%m"), _d.strftime("%b %Y")))
486
+ return res
487
+
488
+ @cached_property
489
+ def start_date(self) -> date:
490
+ return get_date_interval_from_request(self.request, exclude_weekend=True, left_interval_inclusive=True)[0]
491
+
492
+ @cached_property
493
+ def end_date(self) -> date:
494
+ return get_date_interval_from_request(self.request, exclude_weekend=True, right_interval_inclusive=True)[1]
495
+
496
+ @cached_property
497
+ def groupby_classification_group(self) -> ClassificationGroup:
498
+ try:
499
+ return ClassificationGroup.objects.get(id=self.request.GET["groupby_classification_group"])
500
+ except (ValueError, KeyError):
501
+ return get_default_classification_group()
502
+
503
+ @cached_property
504
+ def unique_product(self) -> bool:
505
+ return self.request.GET.get("product") is not None
506
+
507
+ def _rebate_df(self, df_aum_end: pd.Series, **kwargs) -> pd.DataFrame:
508
+ df = pd.DataFrame()
509
+ if apps.is_installed("wbcommission"):
510
+ from wbcommission.viewsets.rebate import RebatePandasView
511
+
512
+ rebate_view = RebatePandasView()
513
+ rebate_view.request = self.request
514
+ df = rebate_view._get_dataframe(**kwargs).drop(["title", "index"], axis=1, errors="ignore").set_index("id")
515
+ if not df.empty:
516
+ return df.reindex(df_aum_end.index, fill_value=0)
517
+ return df
518
+
519
+ def add_messages(self, request, queryset=None, paginated_queryset=None, instance=None, initial=False):
520
+ if self.unique_product:
521
+ return warning(
522
+ request, "Internal trades are excluded from the Net New Money total due to the selected product."
523
+ )
524
+
525
+ def get_dataframe(
526
+ self,
527
+ request,
528
+ queryset,
529
+ with_rebate_df: bool = True,
530
+ with_aum_sparkline: bool = True,
531
+ with_neg_pos_nnm: bool = False,
532
+ **kwargs,
533
+ ):
534
+ groupby = self.request.GET.get("group_by", "PRODUCT")
535
+ groupby_map = ClaimGroupbyChoice.get_map(groupby)
536
+ pivot = groupby_map["pk"]
537
+ pivot_label = groupby_map["title_key"]
538
+
539
+ cts_generator = ConsolidatedTradeSummary(
540
+ queryset,
541
+ self.start_date,
542
+ self.end_date,
543
+ pivot,
544
+ pivot_label,
545
+ classification_group=self.groupby_classification_group,
546
+ )
547
+
548
+ df = cts_generator.df
549
+ if not df.empty:
550
+ df_aum = cts_generator.get_aum_df()
551
+ df_nnm = cts_generator.get_nnm_df(filter_internal_trade=not self.unique_product)
552
+ if with_neg_pos_nnm:
553
+ self.df_nnm_neg = cts_generator.get_nnm_df(
554
+ only_negative=True, filter_internal_trade=not self.unique_product
555
+ )
556
+ self.df_nnm_pos = cts_generator.get_nnm_df(
557
+ only_positive=True, filter_internal_trade=not self.unique_product
558
+ )
559
+
560
+ df_aum_sparkline = pd.DataFrame()
561
+ if with_aum_sparkline:
562
+ df_aum_sparkline = cts_generator.get_aum_sparkline()
563
+ df_rebate = pd.DataFrame()
564
+ if with_rebate_df:
565
+ df_rebate = self._rebate_df(df_aum["sum_aum_end"], **kwargs)
566
+ df_initial_investment_date = cts_generator.get_initial_investment_date_df()
567
+
568
+ df_title = df[["id", "title"]].groupby("id").first()
569
+ df = pd.concat(
570
+ [df_title, df_initial_investment_date, df_aum, df_nnm, df_aum_sparkline, df_rebate], axis=1
571
+ ).reset_index(names="id")
572
+ df["total_performance"] = df["sum_aum_end"] - (df["sum_aum_start"] + df["sum_nnm_total"])
573
+ df["total_performance_perf"] = df["total_performance"] / df["sum_aum_start"]
574
+
575
+ df.loc[df["sum_aum_start"] != 0, "sum_nnm_perf"] = (
576
+ df.loc[df["sum_aum_start"] != 0, "sum_nnm_total"] / df.loc[df["sum_aum_start"] != 0, "sum_aum_start"]
577
+ )
578
+ df = df[(df["sum_shares_start"].abs() > 1) | (df["sum_shares_end"].abs() > 1)]
579
+ df = df.replace([np.inf, -np.inf, np.nan], None)
580
+ if hasattr(self, "df_nnm_neg"):
581
+ self.df_nnm_neg = (
582
+ pd.concat([df_title, self.df_nnm_neg], axis=1)
583
+ .reset_index()
584
+ .replace([np.inf, -np.inf, np.nan], None)
585
+ .sort_values(by="title")
586
+ )
587
+ if hasattr(self, "df_nnm_pos"):
588
+ self.df_nnm_pos = (
589
+ pd.concat([df_title, self.df_nnm_pos], axis=1)
590
+ .reset_index()
591
+ .replace([np.inf, -np.inf, np.nan], None)
592
+ .sort_values(by="title")
593
+ )
594
+ self._columns = df.columns
595
+ return df
596
+
597
+ def get_aggregates(self, request, df):
598
+ if df.empty:
599
+ return {}
600
+ aggregates = {}
601
+ for col in filter(
602
+ lambda x: ("sum" in x and "perf" not in x) or x == "total_performance" or "rebate" in x, df.columns
603
+ ):
604
+ aggregates[col] = {"Σ": format_number(df[col].sum())}
605
+ return aggregates
606
+
607
+ def get_filterset_class(self, request):
608
+ profile = request.user.profile
609
+ if profile.is_internal or request.user.is_superuser:
610
+ return ClaimGroupByFilter
611
+ return CustomerClaimGroupByFilter
612
+
613
+
614
+ class ConsolidatedTradeSummaryDistributionChart(ConsolidatedTradeSummaryTableView):
615
+ WIDGET_TYPE = WidgetType.CHART.value
616
+ IDENTIFIER = "wbportfolio:consolidatedtradesummarydistributionchart"
617
+
618
+ title_config_class = ConsolidatedTradeSummaryDistributionChartTitleConfig
619
+ endpoint_config_class = ConsolidatedTradeSummaryDistributionChartEndpointConfig
620
+ button_config_class = None
621
+ filterset_class = ClaimGroupByFilter
622
+
623
+ # TODO This is not really optimal. We need to change it at some point
624
+ def list(self, request, *args, **kwargs):
625
+ # we copy pasted this function from the chartviewset. Can be optimize
626
+ figure = go.Figure()
627
+ self.request = request
628
+ df = self._get_dataframe(**kwargs, with_rebate_df=False, with_aum_sparkline=False, with_neg_pos_nnm=True)
629
+ if not df.empty:
630
+ figure = self.get_plotly(df)
631
+ figure_json = figure.to_json() # we serialize to use the default PlotlyEncoder
632
+ figure_dict = json.loads(
633
+ figure_json
634
+ ) # we reserialize to be able to hijack the figure config. This adds an extra steps of serialization/deserialization but the overhead is negligable.
635
+ figure_dict["config"] = {"responsive": True, "displaylogo": False}
636
+ figure_dict["useResizeHandler"] = True
637
+ figure_dict["style"] = {"width": "100%", "height": "100%"}
638
+ figure_dict["messages"] = list(self._get_messages(request))
639
+
640
+ return Response(figure_dict)
641
+
642
+ def get_plotly(self, df):
643
+ fig = go.Figure()
644
+ # create the groupby NNM distribution histogram
645
+ df = df.sort_values(by="title")
646
+ for nnm_monthly_colum in self.nnm_monthly_columns:
647
+ if len(self.nnm_monthly_columns) == 1 and hasattr(self, "df_nnm_neg") and hasattr(self, "df_nnm_pos"):
648
+ if nnm_monthly_colum[0] in self.df_nnm_neg.columns:
649
+ fig.add_trace(
650
+ go.Histogram(
651
+ histfunc="sum",
652
+ y=self.df_nnm_neg[nnm_monthly_colum[0]],
653
+ x=self.df_nnm_neg["title"],
654
+ name=nnm_monthly_colum[1] + " (Negative)",
655
+ marker_color="#FF6961",
656
+ )
657
+ )
658
+ if nnm_monthly_colum[0] in self.df_nnm_pos.columns:
659
+ fig.add_trace(
660
+ go.Histogram(
661
+ histfunc="sum",
662
+ y=self.df_nnm_pos[nnm_monthly_colum[0]],
663
+ x=self.df_nnm_pos["title"],
664
+ name=nnm_monthly_colum[1] + " (Positive)",
665
+ marker_color="#77DD77",
666
+ )
667
+ )
668
+ if nnm_monthly_colum[0] in df.columns:
669
+ figure_kwargs = {"name": nnm_monthly_colum[1]}
670
+ if len(self.nnm_monthly_columns) == 1:
671
+ figure_kwargs["marker_color"] = "#D3D3D3"
672
+ figure_kwargs["name"] = nnm_monthly_colum[1] + " (Total)"
673
+ fig.add_trace(go.Histogram(histfunc="sum", y=df[nnm_monthly_colum[0]], x=df["title"], **figure_kwargs))
674
+ if len(self.nnm_monthly_columns) > 1:
675
+ fig.add_trace(
676
+ go.Histogram(
677
+ histfunc="sum",
678
+ y=df["sum_nnm_total"],
679
+ x=df["title"],
680
+ name=f'[{self.start_date.strftime("%Y-%m-%d")}, {self.end_date.strftime("%Y-%m-%d")}]',
681
+ )
682
+ )
683
+ fig.update_layout(
684
+ template="plotly_white",
685
+ margin=dict(l=10, r=10, t=0, b=40),
686
+ showlegend=True,
687
+ legend={
688
+ "orientation": "h",
689
+ "yanchor": "bottom",
690
+ "y": 1,
691
+ "xanchor": "center",
692
+ "x": 0.5,
693
+ },
694
+ )
695
+ return fig
696
+
697
+
698
+ class CumulativeNNMChartView(ConsolidatedTradeSummaryDistributionChart):
699
+ IDENTIFIER = "wbportfolio:cumulativennmchart"
700
+
701
+ title_config_class = CumulativeNNMChartTitleConfig
702
+ endpoint_config_class = CumulativeNNMChartEndpointConfig
703
+ filterset_class = CumulativeNNMChartFilter
704
+
705
+ @cached_property
706
+ def projected_monthly_nnm_target(self) -> int:
707
+ try:
708
+ return int(self.request.GET["projected_monthly_nnm_target"])
709
+ except (ValueError, KeyError):
710
+ return get_monthly_nnm_target()
711
+
712
+ @cached_property
713
+ def hide_projected_monthly_nnm_target(self) -> bool:
714
+ try:
715
+ return self.request.GET["hide_projected_monthly_nnm_target"] == "true"
716
+ except (ValueError, KeyError):
717
+ return False
718
+
719
+ def get_filterset_class(self, request):
720
+ return CumulativeNNMChartFilter
721
+
722
+ def get_plotly(self, df):
723
+ fig = go.Figure()
724
+
725
+ # create the cumulative NNM histogram
726
+ nnm_monthly_columns_dict = dict(self.nnm_monthly_columns)
727
+
728
+ nnm_monthly_columns = df.columns.intersection(nnm_monthly_columns_dict.keys())
729
+ if not nnm_monthly_columns.empty and not df.empty:
730
+ monthly_nnm = df[[*nnm_monthly_columns]].sum().cumsum().rename(index={**nnm_monthly_columns_dict})
731
+
732
+ fig.add_trace(
733
+ go.Histogram(
734
+ histfunc="sum",
735
+ y=monthly_nnm,
736
+ x=monthly_nnm.index,
737
+ autobinx=False,
738
+ showlegend=False,
739
+ yaxis="y",
740
+ marker_color="darkgrey",
741
+ name="Monthly Cumulative NNM",
742
+ )
743
+ )
744
+ if not self.hide_projected_monthly_nnm_target:
745
+ a = self.projected_monthly_nnm_target
746
+ x = np.linspace(0, monthly_nnm.shape[0], monthly_nnm.shape[0] - 1)
747
+ target_points = a * x + a
748
+ fig.add_trace(
749
+ go.Scatter(
750
+ x=monthly_nnm.index[0:],
751
+ y=target_points,
752
+ mode="lines",
753
+ text="Projected NNM target",
754
+ line=dict(color="red", width=4),
755
+ showlegend=False,
756
+ )
757
+ )
758
+ return fig
759
+
760
+
761
+ class ProfitAndLossPandasView(ClaimPermissionMixin, ExportPandasAPIViewSet):
762
+ IDENTIFIER = "commission:pnl"
763
+ # LIST_DOCUMENTATION = "wbportfolio/commission/viewsets/documentation/profitandlosspandasview.md"
764
+ # Averaging method based on https://www.tradingtechnologies.com/xtrader-help/fix-adapter-reference/pl-calculation-algorithm/understanding-pl-calculations/
765
+ pandas_fields = pf.PandasFields(
766
+ fields=(
767
+ pf.PKField(key="id", label="ID"),
768
+ pf.CharField(key="title", label="Product"),
769
+ pf.FloatField(
770
+ key="unrealized_pnl",
771
+ label="Realized P&L",
772
+ precision=2,
773
+ decorators=(decorator(decorator_type="text", position="left", value="$"),),
774
+ help_text="P&L_unrealized (points) = (Theoretical Exit Price - Avg Buy Price) * total_shares",
775
+ ),
776
+ pf.FloatField(
777
+ key="realized_pnl",
778
+ label="Unrealized P&L",
779
+ precision=2,
780
+ decorators=(decorator(decorator_type="text", position="left", value="$"),),
781
+ help_text="P&L_realized (points) = (Avg Sell Price - Avg Buy Price) * total_shares_sold",
782
+ ),
783
+ pf.FloatField(
784
+ key="total_pnl",
785
+ label="Total P&L",
786
+ precision=2,
787
+ decorators=(decorator(decorator_type="text", position="left", value="$"),),
788
+ help_text="P&L_total (points) = P&L_unrealized + P&L_realized",
789
+ ),
790
+ pf.FloatField(
791
+ key="total_invested",
792
+ label="Total AUM",
793
+ precision=2,
794
+ decorators=(decorator(decorator_type="text", position="left", value="$"),),
795
+ help_text="total_invested = Avg Buy Price * total_shares",
796
+ ),
797
+ pf.BooleanField(
798
+ key="is_invested",
799
+ label="Invested",
800
+ ),
801
+ pf.FloatField(
802
+ key="performance",
803
+ label="Performance",
804
+ precision=2,
805
+ percent=True,
806
+ help_text="performance = P&L_total / total_invested",
807
+ ),
808
+ )
809
+ )
810
+
811
+ queryset = Claim.objects.filter(account__owner__isnull=False)
812
+
813
+ filterset_class = ProfitAndLossPandasFilter
814
+
815
+ search_fields = ("title",)
816
+ ordering_fields = ["title", "unrealized_pnl", "realized_pnl", "total_pnl", "total_invested", "performance"]
817
+
818
+ display_config_class = ProfitAndLossPandasDisplayConfig
819
+ title_config_class = ProfitAndLossPandasTitleConfig
820
+ endpoint_config_class = ProfitAndLossPandasEndpointConfig
821
+
822
+ def get_aggregates(self, request, df):
823
+ if df.empty:
824
+ return {}
825
+ return {
826
+ "unrealized_pnl": {"Σ": format_number(df.unrealized_pnl.sum())},
827
+ "realized_pnl": {"Σ": format_number(df.realized_pnl.sum())},
828
+ "total_pnl": {"Σ": format_number(df.total_pnl.sum())},
829
+ "total_invested": {"Σ": format_number(df.total_invested.sum())},
830
+ }
831
+
832
+ def get_queryset(self):
833
+ start_date, end_date = get_date_interval_from_request(self.request, exclude_weekend=True)
834
+ return (
835
+ super()
836
+ .get_queryset()
837
+ .annotate(
838
+ net_value=InstrumentPrice.subquery_closest_value(
839
+ "net_value", instrument_pk_name="product__pk", date_lookup="exact"
840
+ ),
841
+ fx_rate=CurrencyFXRates.get_fx_rates_subquery("date", lookup_expr="exact"),
842
+ total_value=ExpressionWrapper(
843
+ F("net_value") * F("shares") * F("fx_rate"), output_field=DecimalField()
844
+ ),
845
+ date_end=Least(F("product__last_valuation_date"), Value(end_date)),
846
+ net_value_end=InstrumentPrice.subquery_closest_value(
847
+ "net_value", date_name="date_end", instrument_pk_name="product__pk", date_lookup="exact"
848
+ ),
849
+ fx_rate_end=CurrencyFXRates.get_fx_rates_subquery("date_end", lookup_expr="exact"),
850
+ price_end=ExpressionWrapper(F("net_value_end") * F("fx_rate_end"), output_field=DecimalField()),
851
+ )
852
+ )
853
+
854
+ def get_dataframe(self, request, queryset, **kwargs):
855
+ start_date, end_date = get_date_interval_from_request(self.request, exclude_weekend=True)
856
+ if not start_date or not end_date:
857
+ return pd.DataFrame()
858
+
859
+ df = pd.DataFrame(queryset.values("shares", "total_value", "price_end", "account__owner__id", "product"))
860
+ if not df.empty:
861
+ df[["shares", "total_value"]] = df[["shares", "total_value"]].astype("float")
862
+
863
+ df_price_end = (
864
+ df[["price_end", "account__owner__id", "product"]].groupby(["account__owner__id", "product"]).mean()
865
+ )
866
+ df_buy = df[["shares", "total_value", "account__owner__id", "product"]][df["shares"] > 0]
867
+ df_buy = df_buy.groupby(["account__owner__id", "product"]).agg({"shares": "sum", "total_value": "sum"})
868
+ df_buy["total_value"] = df_buy.total_value / df_buy.shares
869
+ df_buy = df_buy.rename(columns={"total_value": "avg_buy_price", "shares": "total_buy_shares"})
870
+
871
+ df_sell = df[["shares", "total_value", "account__owner__id", "product"]][df["shares"] < 0]
872
+ df_sell = df_sell.groupby(["account__owner__id", "product"]).agg({"shares": "sum", "total_value": "sum"})
873
+ df_sell = df_sell.abs()
874
+ df_sell["total_value"] = df_sell.total_value / df_sell.shares
875
+ df_sell = df_sell.rename(columns={"total_value": "avg_sell_price", "shares": "total_sell_shares"})
876
+
877
+ df = pd.concat([df_buy, df_sell], axis=1).fillna(0)
878
+ df["realized_pnl"] = (df.avg_sell_price - df.avg_buy_price) * df.total_sell_shares
879
+ df["total_shares"] = df.total_buy_shares - df.total_sell_shares
880
+
881
+ df["unrealized_pnl"] = (df_price_end["price_end"] - df.avg_buy_price) * df.total_shares
882
+ df["total_pnl"] = df.unrealized_pnl + df.realized_pnl
883
+ df = df.groupby(level=0).sum().reset_index()
884
+
885
+ df["total_invested"] = df.avg_buy_price * df.total_buy_shares
886
+
887
+ df["performance"] = df.total_pnl.astype(float).divide(df.total_invested.astype(float))
888
+
889
+ df["id"] = df["account__owner__id"]
890
+ df["title"] = df["account__owner__id"].map(
891
+ dict(Entry.objects.filter(id__in=df["id"]).values_list("id", "computed_str"))
892
+ )
893
+ return df.where(pd.notnull(df), None)
894
+
895
+
896
+ class NegativeTermimalAccountPerProductModelViewSet(ClaimPermissionMixin, wb_viewsets.ReadOnlyInfiniteModelViewSet):
897
+ IDENTIFIER = "wbportfolio:negativeaccountproduct"
898
+
899
+ serializer_class = NegativeTermimalAccountPerProductModelSerializer
900
+ filterset_class = NegativeTermimalAccountPerProductFilterSet
901
+ queryset = Claim.objects.filter(account__is_active=True)
902
+
903
+ search_fields = ("account__title", "product__computed_str")
904
+ ordering_fields = ("account__title", "product__computed_str", "sum_shares")
905
+ ordering = "sum_shares"
906
+
907
+ display_config_class = NegativeTermimalAccountPerProductDisplayConfig
908
+ title_config_class = NegativeTermimalAccountPerProductTitleConfig
909
+ endpoint_config_class = NegativeTermimalAccountPerProductEndpointConfig
910
+
911
+ def get_aggregates(self, queryset, paginated_queryset):
912
+ return {
913
+ "sum_shares": {"Σ": format_number(queryset.aggregate(s=Sum("sum_shares"))["s"] or 0)},
914
+ }
915
+
916
+ def get_queryset(self):
917
+ return (
918
+ super()
919
+ .get_queryset()
920
+ .select_related("account", "product")
921
+ .values("account", "product")
922
+ .annotate(
923
+ sum_shares=Sum("shares"),
924
+ id=ExpressionWrapper(Concat(F("account__id"), Value("-"), F("product__id")), output_field=CharField()),
925
+ account_repr=F("account__computed_str"),
926
+ product_repr=F("product__computed_str"),
927
+ account_id=F("account__id"),
928
+ product_id=F("product__id"),
929
+ )
930
+ .filter(sum_shares__lt=0, account__isnull=False)
931
+ .exclude(status=Claim.Status.WITHDRAWN)
932
+ .values("account_repr", "product_repr", "account_id", "product_id", "sum_shares", "id")
933
+ )