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,211 @@
1
+ from decimal import Decimal
2
+
3
+ from django.apps import apps
4
+ from django.db import models
5
+ from django.dispatch import receiver
6
+ from wbcore.contrib.io.mixins import ImportMixin
7
+ from wbcore.signals import pre_merge
8
+ from wbfdm.models.instruments.instruments import Instrument
9
+ from wbfdm.signals import add_instrument_to_investable_universe
10
+
11
+
12
+ class ShareMixin(models.Model):
13
+ shares = models.DecimalField(
14
+ max_digits=15,
15
+ decimal_places=4,
16
+ null=True,
17
+ blank=True,
18
+ help_text="The number of shares that were traded.",
19
+ verbose_name="Shares",
20
+ )
21
+ price = models.DecimalField(
22
+ max_digits=16,
23
+ decimal_places=4,
24
+ null=True,
25
+ blank=True,
26
+ help_text="The price per share.",
27
+ verbose_name="Price",
28
+ )
29
+
30
+ price_gross = models.DecimalField(
31
+ max_digits=16,
32
+ decimal_places=4,
33
+ null=True,
34
+ blank=True,
35
+ help_text="The gross price per share.",
36
+ verbose_name="Gross Price",
37
+ )
38
+
39
+ class Meta:
40
+ abstract = True
41
+
42
+
43
+ class Transaction(ImportMixin, models.Model):
44
+ class Type(models.TextChoices):
45
+ # Standart Asset Types
46
+ TRADE = "Trade", "Trade"
47
+ DIVIDEND_TRANSACTION = "DividendTransaction", "Dividend Transaction"
48
+ EXPIRY = "Expiry", "Expiry"
49
+ FEES = "Fees", "Fees"
50
+
51
+ transaction_type = models.CharField(max_length=255, verbose_name="Type", choices=Type.choices, default=Type.TRADE)
52
+
53
+ portfolio = models.ForeignKey(
54
+ "wbportfolio.Portfolio", related_name="transactions", on_delete=models.PROTECT, verbose_name="Portfolio"
55
+ )
56
+
57
+ underlying_instrument = models.ForeignKey(
58
+ to="wbfdm.Instrument",
59
+ related_name="transactions",
60
+ limit_choices_to=models.Q(children__isnull=True),
61
+ on_delete=models.PROTECT,
62
+ verbose_name="Underlying Instrument",
63
+ help_text="The instrument that is this transaction.",
64
+ )
65
+
66
+ transaction_date = models.DateField(
67
+ verbose_name="Trade Date",
68
+ help_text="The date that this transaction was traded.",
69
+ )
70
+ book_date = models.DateField(
71
+ null=True,
72
+ blank=True,
73
+ verbose_name="Trade Date",
74
+ help_text="The date that this transaction was booked.",
75
+ )
76
+ value_date = models.DateField(
77
+ null=True,
78
+ blank=True,
79
+ verbose_name="Value Date",
80
+ help_text="The date that this transaction was valuated.",
81
+ )
82
+
83
+ currency = models.ForeignKey(
84
+ "currency.Currency",
85
+ related_name="transactions",
86
+ on_delete=models.PROTECT,
87
+ verbose_name="Currency",
88
+ )
89
+ currency_fx_rate = models.DecimalField(
90
+ max_digits=14, decimal_places=8, default=Decimal(1.0), verbose_name="FOREX rate"
91
+ )
92
+ total_value = models.DecimalField(
93
+ max_digits=20, decimal_places=4, null=True, blank=True, verbose_name="Total Value"
94
+ )
95
+ total_value_fx_portfolio = models.DecimalField(
96
+ max_digits=20, decimal_places=4, null=True, blank=True, verbose_name="Total Value Fx Portfolio"
97
+ )
98
+ total_value_gross = models.DecimalField(
99
+ max_digits=20, decimal_places=4, null=True, blank=True, verbose_name="Total Value Gross"
100
+ )
101
+ total_value_gross_fx_portfolio = models.DecimalField(
102
+ max_digits=20, decimal_places=4, null=True, blank=True, verbose_name="Total Value Gross Fx Portfolio"
103
+ )
104
+ external_id = models.CharField(
105
+ max_length=255,
106
+ null=True,
107
+ blank=True,
108
+ help_text="An external identifier that was supplied.",
109
+ verbose_name="External Identifier",
110
+ )
111
+ comment = models.TextField(default="", verbose_name="Comment", blank=True)
112
+
113
+ def save(self, *args, **kwargs):
114
+ if not getattr(self, "currency", None) and self.underlying_instrument:
115
+ self.currency = self.underlying_instrument.currency
116
+ if not self.currency_fx_rate:
117
+ self.currency_fx_rate = self.underlying_instrument.currency.convert(
118
+ self.transaction_date, self.portfolio.currency, exact_lookup=True
119
+ )
120
+ if not self.transaction_type:
121
+ self.transaction_type = self.__class__.__name__
122
+ if not self.value_date:
123
+ self.value_date = self.transaction_date
124
+ # try:
125
+ # # we try to find the next valid date (i.e. the one with position on the underlying instrument"
126
+ # self.value_date = (
127
+ # self.underlying_instrument.valuations.filter(date__gt=self.transaction_date).earliest("date").date
128
+ # )
129
+ # except ObjectDoesNotExist:
130
+ # self.value_date = (self.transaction_date + BDay(1)).date()
131
+ if not self.book_date:
132
+ self.book_date = self.transaction_date
133
+ if (
134
+ self.total_value is not None
135
+ and self.currency_fx_rate is not None
136
+ and self.total_value_fx_portfolio is None
137
+ ):
138
+ self.total_value_fx_portfolio = self.total_value * self.currency_fx_rate
139
+
140
+ if self.total_value is not None and self.total_value_gross is None and self.total_value_gross is None:
141
+ self.total_value_gross = self.total_value
142
+
143
+ if (
144
+ self.currency_fx_rate is not None
145
+ and self.total_value_gross is not None
146
+ and self.total_value_gross_fx_portfolio is None
147
+ ):
148
+ self.total_value_gross_fx_portfolio = self.total_value_gross * self.currency_fx_rate
149
+
150
+ super().save(*args, **kwargs)
151
+
152
+ def __str__(self):
153
+ return f"{self.total_value} - {self.transaction_date:%d.%m.%Y} : {str(self.underlying_instrument)} (in {str(self.portfolio)})"
154
+
155
+ def get_casted_model(self):
156
+ return apps.get_model(app_label="wbportfolio", model_name=self.transaction_type)
157
+
158
+ def get_casted_transaction(self) -> models.Model:
159
+ """
160
+ Cast the asset into its child representative
161
+ """
162
+ model = self.get_casted_model()
163
+ return model.objects.get(pk=self.pk)
164
+
165
+ class Meta:
166
+ verbose_name = "Transaction"
167
+ verbose_name_plural = "Transactions"
168
+ indexes = [
169
+ models.Index(fields=["underlying_instrument", "transaction_date"]),
170
+ # models.Index(fields=["date", "underlying_instrument"]),
171
+ ]
172
+
173
+ objects = models.Manager()
174
+
175
+ @classmethod
176
+ def get_representation_value_key(cls):
177
+ return "id"
178
+
179
+ @classmethod
180
+ def get_representation_label_key(cls):
181
+ return "{{total_value}}{{transaction_date}}"
182
+
183
+ @classmethod
184
+ def get_endpoint_basename(cls):
185
+ return "wbportfolio:transaction"
186
+
187
+
188
+ @receiver(pre_merge, sender="wbfdm.Instrument")
189
+ def pre_merge_instrument(sender: models.Model, merged_object: "Instrument", main_object: "Instrument", **kwargs):
190
+ """
191
+ Simply reassign the transactions linked to the merged instrument to the main instrument
192
+ """
193
+ merged_object.transactions.update(underlying_instrument=main_object)
194
+
195
+
196
+ @receiver(add_instrument_to_investable_universe, sender="wbfdm.Instrument")
197
+ def add_instrument_to_investable_universe_from_transactions(sender: models.Model, **kwargs) -> list[int]:
198
+ """
199
+ register all instrument linked to assets as within the investible universe
200
+ """
201
+ return list(
202
+ (
203
+ Instrument.objects.annotate(
204
+ transaction_exists=models.Exists(
205
+ Transaction.objects.filter(underlying_instrument=models.OuterRef("pk"))
206
+ )
207
+ ).filter(transaction_exists=True)
208
+ )
209
+ .distinct()
210
+ .values_list("id", flat=True)
211
+ )
@@ -0,0 +1,12 @@
1
+ from wbfdm.models import Instrument
2
+ from wbportfolio.models import Index, Product
3
+
4
+
5
+ def get_casted_portfolio_instrument(instrument: Instrument) -> Product | Index | None:
6
+ try:
7
+ return Product.objects.get(id=instrument.id)
8
+ except Product.DoesNotExist:
9
+ try:
10
+ return Index.objects.get(id=instrument.id)
11
+ except Index.DoesNotExist:
12
+ return None
@@ -0,0 +1,13 @@
1
+ from wbportfolio.models import PortfolioRole
2
+
3
+
4
+ def is_manager(request):
5
+ return PortfolioRole.is_manager(request.user.profile)
6
+
7
+
8
+ def is_portfolio_manager(request):
9
+ return PortfolioRole.is_portfolio_manager(request.user.profile)
10
+
11
+
12
+ def is_analyst(request):
13
+ return PortfolioRole.is_analyst(request.user.profile)
File without changes
File without changes
@@ -0,0 +1 @@
1
+ from .handler import TradingService
@@ -0,0 +1,164 @@
1
+ from datetime import date
2
+ from decimal import Decimal
3
+
4
+ from django.core.exceptions import ValidationError
5
+ from wbportfolio.pms.typing import Portfolio, Trade, TradeBatch
6
+
7
+
8
+ class TradingService:
9
+ """
10
+ This class represents the trading service. It can be instantiated either with the target portfolio and the effective portfolio or given a direct list of trade
11
+ In any case, it will compute all three states
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ trade_date: date,
17
+ effective_portfolio: Portfolio | None = None,
18
+ target_portfolio: Portfolio | None = None,
19
+ trades_batch: TradeBatch | None = None,
20
+ total_value: Decimal = None,
21
+ ):
22
+ if not target_portfolio and not trades_batch:
23
+ raise ValueError("Either target positions or trades needs to be provided")
24
+ self.total_value = total_value
25
+ self.trade_date = trade_date
26
+ # If effective portfoolio and trades batch is provided, we ensure the trade batch contains at least one trade for every position
27
+ if effective_portfolio and trades_batch:
28
+ trades_batch = self.build_trade_batch(effective_portfolio, trades_batch=trades_batch)
29
+ # if no trade but a effective portfolio is provided, we get the trade batch only from the effective portofolio (and the target portfolio if provided, but optional. Without it, the trade delta weight will be 0 )
30
+ elif not trades_batch and effective_portfolio:
31
+ # If no trade batch is provided but effetive_portfolio is, we estimate the trade from the given portfolios
32
+ trades_batch = self.build_trade_batch(effective_portfolio, target_portfolio=target_portfolio)
33
+ # Finally, we compute the target portfolio
34
+ if trades_batch and not target_portfolio:
35
+ target_portfolio = trades_batch.convert_to_portfolio()
36
+
37
+ self.trades_batch = trades_batch
38
+ self.effective_portfolio = effective_portfolio
39
+ self.target_portfolio = target_portfolio
40
+
41
+ @property
42
+ def errors(self) -> list[str]:
43
+ """
44
+ Returned the list of errors stored during the validation process. Can only be called after is_valid
45
+ """
46
+ if not hasattr(self, "_errors"):
47
+ msg = "You must call `.is_valid()` before accessing `.errors`."
48
+ raise AssertionError(msg)
49
+ return self._errors
50
+
51
+ @property
52
+ def validated_trades(self) -> list[Trade]:
53
+ """
54
+ Returned the list of validated trade stored during the validation process. Can only be called after is_valid
55
+ """
56
+ if not hasattr(self, "_validated_trades"):
57
+ msg = "You must call `.is_valid()` before accessing `.validated_trades`."
58
+ raise AssertionError(msg)
59
+ return self._validated_trades
60
+
61
+ def run_validation(self, validated_trades: list[Trade]):
62
+ """
63
+ Test the given value against all the validators on the field,
64
+ and either raise a `ValidationError` or simply return.
65
+ """
66
+ TradeBatch(validated_trades).validate()
67
+ if self.effective_portfolio:
68
+ for trade in validated_trades:
69
+ if (
70
+ trade.effective_weight
71
+ and trade.underlying_instrument not in self.effective_portfolio.positions_map
72
+ ):
73
+ raise ValidationError("All effective position needs to be matched with a validated trade")
74
+
75
+ def build_trade_batch(
76
+ self,
77
+ effective_portfolio: Portfolio,
78
+ target_portfolio: Portfolio | None = None,
79
+ trades_batch: TradeBatch | None = None,
80
+ ) -> TradeBatch:
81
+ """
82
+ Given combination of effective portfolio and either a trades batch or a target portfolio, ensure all theres variables are set
83
+
84
+ Args:
85
+ effective_portfolio: The effective portfolio
86
+ target_portfolio: The optional target portfolio
87
+ trades_batch: The optional trades batch
88
+
89
+ Returns: The normalized trades batch
90
+ """
91
+ instruments = list(effective_portfolio.positions_map.keys())
92
+ if target_portfolio:
93
+ instruments.extend(list(target_portfolio.positions_map.keys()))
94
+ if trades_batch:
95
+ instruments.extend(list(trades_batch.trades_map.keys()))
96
+ _trades: list[Trade] = []
97
+ for instrument in set(instruments):
98
+ effective_weight = target_weight = 0
99
+ effective_shares = target_shares = 0
100
+ instrument_type = currency = None
101
+ if effective_pos := effective_portfolio.positions_map.get(instrument, None):
102
+ effective_weight = target_weight = effective_pos.weighting
103
+ effective_shares = target_shares = effective_pos.shares
104
+ instrument_type, currency = effective_pos.instrument_type, effective_pos.currency
105
+ if target_portfolio and (target_pos := target_portfolio.positions_map.get(instrument, None)):
106
+ target_weight = target_pos.weighting
107
+ target_shares = target_pos.shares
108
+ if trades_batch and (trade := trades_batch.trades_map.get(instrument, None)):
109
+ effective_weight, target_weight = trade.effective_weight, trade.target_weight
110
+ effective_shares, target_shares = trade.effective_shares, trade.target_shares
111
+ instrument_type, currency = trade.instrument_type, trade.currency
112
+
113
+ _trades.append(
114
+ Trade(
115
+ underlying_instrument=instrument,
116
+ effective_weight=effective_weight,
117
+ target_weight=target_weight,
118
+ effective_shares=effective_shares,
119
+ target_shares=target_shares,
120
+ date=self.trade_date,
121
+ instrument_type=instrument_type,
122
+ currency=currency,
123
+ )
124
+ )
125
+ return TradeBatch(tuple(_trades))
126
+
127
+ def is_valid(self, ignore_error: bool = False) -> bool:
128
+ """
129
+ Validate the trade batch against a set of default rules. Populate the validated_trades and errors property.
130
+ Ignore error by default
131
+ Args:
132
+ ignore_error: If true, will raise the error. False by default
133
+
134
+ Returns: True if the trades batch is valid
135
+ """
136
+ if not hasattr(self, "_validated_trades"):
137
+ self._validated_trades = []
138
+ self._errors = []
139
+ # Run validation for every trade. If a trade is not valid, we simply exclude it from the validated trades list
140
+ for _, trade in self.trades_batch.trades_map.items():
141
+ try:
142
+ trade.validate()
143
+ self._validated_trades.append(trade)
144
+ except ValidationError as exc:
145
+ self._errors.append(exc.message)
146
+ try:
147
+ # Check the overall validity of the trade batch. If this fail, we consider all trade invalids
148
+ self.run_validation(self._validated_trades)
149
+ except ValidationError as exc:
150
+ self._validated_trades = []
151
+ self._errors.append(exc.message)
152
+
153
+ if self._errors and not ignore_error:
154
+ raise ValidationError(self.errors)
155
+
156
+ return not bool(self._errors)
157
+
158
+ def normalize(self):
159
+ """
160
+ Normalize the instantiate trades batch so that the target weight is 100%
161
+ """
162
+ self.trades_batch = TradeBatch(
163
+ [trade.normalize_target(self.trades_batch.total_target_weight) for trade in self.trades_batch.trades]
164
+ )
@@ -0,0 +1,194 @@
1
+ from dataclasses import asdict, dataclass, field, fields
2
+ from datetime import date as date_lib
3
+ from decimal import Decimal
4
+
5
+ import pandas as pd
6
+ from django.core.exceptions import ValidationError
7
+ from django.utils.functional import cached_property
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Valuation:
12
+ instrument: int
13
+ net_value: Decimal
14
+ outstanding_shares: Decimal = Decimal(0)
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Position:
19
+ underlying_instrument: int
20
+ instrument_type: int
21
+ weighting: Decimal
22
+ currency: int
23
+ date: date_lib
24
+
25
+ asset_valuation_date: date_lib | None = None
26
+ portfolio_created: int = None
27
+ exchange: int = None
28
+ is_estimated: bool = False
29
+ country: int = None
30
+ shares: Decimal | None = None
31
+ is_cash: bool = False
32
+ primary_classification: int = None
33
+ favorite_classification: int = None
34
+ market_capitalization_usd: float = None
35
+ currency_fx_rate: float = 1
36
+ market_share: float = None
37
+ daily_liquidity: float = None
38
+ volume_usd: float = None
39
+ price: float = None
40
+
41
+ def __add__(self, other):
42
+ return Position(
43
+ weighting=self.weighting + other.weighting,
44
+ shares=self.shares + other.shares if (self.shares is not None and other.shares is not None) else None,
45
+ **{f.name: getattr(self, f.name) for f in fields(Position) if f.name not in ["weighting", "shares"]},
46
+ )
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class Portfolio:
51
+ positions: tuple[Position]
52
+ positions_map: dict[Position] = field(init=False, repr=False)
53
+
54
+ def __post_init__(self):
55
+ positions_map = {}
56
+ for pos in self.positions:
57
+ if pos.underlying_instrument in positions_map:
58
+ positions_map[pos.underlying_instrument] += pos
59
+ else:
60
+ positions_map[pos.underlying_instrument] = pos
61
+ object.__setattr__(self, "positions_map", positions_map)
62
+
63
+ @cached_property
64
+ def total_weight(self):
65
+ return round(sum([pos.weighting for pos in self.positions]), 4)
66
+
67
+ @cached_property
68
+ def total_shares(self):
69
+ return sum([pos.target_shares for pos in self.positions if pos.target_shares is not None])
70
+
71
+ def to_df(self):
72
+ return pd.DataFrame([asdict(pos) for pos in self.positions])
73
+
74
+ def __len__(self):
75
+ return len(self.positions)
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class Trade:
80
+ underlying_instrument: int
81
+ instrument_type: str
82
+ currency: int
83
+ date: date_lib
84
+
85
+ effective_weight: Decimal
86
+ target_weight: Decimal
87
+ id: int | None = None
88
+ effective_shares: Decimal = None
89
+ target_shares: Decimal = None
90
+
91
+ def __add__(self, other):
92
+ return Trade(
93
+ underlying_instrument=self.underlying_instrument,
94
+ effective_weight=self.effective_weight + other.effective_weight,
95
+ target_weight=self.target_weight + other.target_weight,
96
+ target_shares=self.target_shares + other.target_shares
97
+ if (self.target_shares is not None and other.target_shares is not None)
98
+ else None,
99
+ effective_shares=self.effective_shares + other.effective_shares
100
+ if (self.effective_shares is not None and other.effective_shares is not None)
101
+ else None,
102
+ **{
103
+ f.name: getattr(self, f.name)
104
+ for f in fields(Trade)
105
+ if f.name
106
+ not in [
107
+ "effective_weight",
108
+ "target_weight",
109
+ "target_shares",
110
+ "effective_shares",
111
+ "underlying_instrument",
112
+ ]
113
+ },
114
+ )
115
+
116
+ @cached_property
117
+ def delta_weight(self) -> Decimal:
118
+ return self.target_weight - self.effective_weight
119
+
120
+ def validate(self):
121
+ if self.effective_weight < 0 or self.effective_weight > 1.0:
122
+ raise ValidationError("Effective Weight needs to be in range [0, 1]")
123
+ if self.target_weight < 0 or self.target_weight > 1.0:
124
+ raise ValidationError("Target Weight needs to be in range [0, 1]")
125
+
126
+ def normalize_target(self, total_target_weight: Decimal):
127
+ t = Trade(
128
+ target_weight=self.target_weight / total_target_weight if total_target_weight else self.target_weight,
129
+ target_shares=self.target_shares / total_target_weight
130
+ if (self.target_shares and total_target_weight)
131
+ else self.target_shares,
132
+ **{
133
+ f.name: getattr(self, f.name)
134
+ for f in fields(Trade)
135
+ if f.name not in ["target_weight", "target_shares"]
136
+ },
137
+ )
138
+ return t
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class TradeBatch:
143
+ trades: tuple[Trade]
144
+ trades_map: dict[Trade] = field(init=False, repr=False)
145
+
146
+ def __post_init__(self):
147
+ trade_map = {}
148
+ for trade in self.trades:
149
+ if trade.underlying_instrument in trade_map:
150
+ trade_map[trade.underlying_instrument] += trade
151
+ else:
152
+ trade_map[trade.underlying_instrument] = trade
153
+ object.__setattr__(self, "trades_map", trade_map)
154
+
155
+ @cached_property
156
+ def total_target_weight(self) -> Decimal:
157
+ return round(sum([trade.target_weight for trade in self.trades]), 4)
158
+
159
+ @cached_property
160
+ def total_effective_weight(self) -> Decimal:
161
+ return round(sum([trade.effective_weight for trade in self.trades]), 4)
162
+
163
+ @cached_property
164
+ def total_shares(self) -> Decimal:
165
+ return sum([trade.target_shares for trade in self.trades if trade.target_shares is not None]) or Decimal(0)
166
+
167
+ @cached_property
168
+ def totat_abs_delta_weight(self) -> Decimal:
169
+ return sum([abs(trade.delta_weight) for trade in self.trades])
170
+
171
+ def __add__(self, other):
172
+ return TradeBatch(tuple(self.trades + other.trades))
173
+
174
+ def __len__(self):
175
+ return len(self.trades)
176
+
177
+ def validate(self):
178
+ if float(self.total_target_weight) != 1.0: # we do that to remove decimal over precision
179
+ raise ValidationError(f"Total Weight cannot be different than 1 ({float(self.total_target_weight)})")
180
+
181
+ def convert_to_portfolio(self):
182
+ positions = []
183
+ for instrument, trade in self.trades_map.items():
184
+ positions.append(
185
+ Position(
186
+ underlying_instrument=trade.underlying_instrument,
187
+ instrument_type=trade.instrument_type,
188
+ weighting=trade.target_weight,
189
+ shares=trade.target_shares,
190
+ currency=trade.currency,
191
+ date=trade.date,
192
+ )
193
+ )
194
+ return Portfolio(tuple(positions))
@@ -0,0 +1,6 @@
1
+ from dynamic_preferences.registries import global_preferences_registry
2
+
3
+
4
+ def get_monthly_nnm_target(*args, **kwargs):
5
+ global_preferences = global_preferences_registry.manager()
6
+ return global_preferences["wbportfolio__monthly_nnm_target"]
File without changes
@@ -0,0 +1,74 @@
1
+ from datetime import datetime
2
+ from io import BytesIO
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ import pandas as pd
6
+ from pandas.tseries.offsets import BMonthEnd
7
+ from wbfdm.models import InstrumentList
8
+ from wbreport.mixins import ReportMixin
9
+
10
+ if TYPE_CHECKING:
11
+ from wbcore.contrib.authentication.models import User
12
+ from wbreport.models import Report, ReportVersion
13
+
14
+
15
+ class ReportClass(ReportMixin):
16
+ @classmethod
17
+ def parse_parameters(cls, parameters: dict[str, str]) -> dict[str, Any]:
18
+ return {
19
+ "end": datetime.strptime(parameters["end"], "%Y-%m-%d").date(),
20
+ }
21
+
22
+ @classmethod
23
+ def get_next_parameters(cls, parameters: dict[str, Any]) -> dict[str, Any]:
24
+ parse_parameters = cls.parse_parameters(parameters)
25
+ return {
26
+ "end": datetime.strftime((parse_parameters["end"] + BMonthEnd(1)).date(), "%Y-%m-%d"),
27
+ }
28
+
29
+ @classmethod
30
+ def get_version_date(cls, parameters) -> datetime.date:
31
+ parameters = cls.parse_parameters(parameters)
32
+ return parameters["end"]
33
+
34
+ @classmethod
35
+ def get_version_title(cls, report_title: str, parameters: dict[str, Any]) -> str:
36
+ parameters = cls.parse_parameters(parameters)
37
+ return f"{report_title} - {parameters['end']:%b}"
38
+
39
+ @classmethod
40
+ def has_view_permission(cls, report: "Report", user: "User") -> bool:
41
+ return user.has_perms(["wbreport.view_report", "wbportfolio.view_assetposition"])
42
+
43
+ @classmethod
44
+ def get_context(cls, version: "ReportVersion") -> dict[str, Any]:
45
+ instrument_list: "InstrumentList" = version.report.content_object
46
+ end_date = version.parameters.get("end")
47
+ positions = []
48
+
49
+ for instrument in instrument_list.instruments.all():
50
+ if portfolio := instrument.primary_portfolio:
51
+ for position in portfolio.assets.filter(date=end_date):
52
+ positions.append(
53
+ {
54
+ "portfolio": instrument.name,
55
+ "isin": position.underlying_instrument.isin,
56
+ "title": position.underlying_instrument.name_repr,
57
+ "instrument_type": position.underlying_instrument.security_instrument_type.short_name,
58
+ "weight": float(position.weighting),
59
+ "date": position.date.strftime("%Y-%m-%d"),
60
+ }
61
+ )
62
+
63
+ return {"positions": positions}
64
+
65
+ @classmethod
66
+ def generate_file(cls, context: dict[str, Any]) -> BytesIO:
67
+ stream = BytesIO()
68
+ if positions := context.get("positions", None):
69
+ df = pd.DataFrame(positions).sort_values(by=["weight"], ascending=False)
70
+ writer = pd.ExcelWriter(stream, engine="xlsxwriter")
71
+ for portfolio, dff in df.groupby("portfolio"):
72
+ dff.to_excel(writer, sheet_name=portfolio[0:31], index=False)
73
+ writer.save()
74
+ return stream
File without changes