wbportfolio 1.55.3__py2.py3-none-any.whl → 1.55.5__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.

@@ -58,13 +58,12 @@ class ConsolidatedTradeSummary:
58
58
  self.pivot_label = pivot_label
59
59
  self.queryset = self.queryset.annotate(
60
60
  internal_trade=F("trade__marked_as_internal"),
61
- date_considered=ExpressionWrapper(
62
- Greatest("trade__transaction_date", "date") + 1, output_field=DateField()
63
- ),
61
+ valid_date=Greatest("trade__transaction_date", "date"),
62
+ date_considered=ExpressionWrapper(F("valid_date") + 1, output_field=DateField()),
64
63
  net_value=InstrumentPrice.subquery_closest_value(
65
- "net_value", date_name="date_considered", instrument_pk_name="product__pk", date_lookup="exact"
64
+ "net_value", date_name="valid_date", instrument_pk_name="product__pk", date_lookup="exact"
66
65
  ),
67
- fx_rate=CurrencyFXRates.get_fx_rates_subquery("date_considered", lookup_expr="exact"),
66
+ fx_rate=CurrencyFXRates.get_fx_rates_subquery("valid_date", lookup_expr="exact"),
68
67
  aum=ExpressionWrapper(F("fx_rate") * F("net_value") * F("shares"), output_field=DecimalField()),
69
68
  )
70
69
  self.queryset = Account.annotate_root_account_info(self.queryset)
@@ -158,8 +157,8 @@ class ConsolidatedTradeSummary:
158
157
  df = df.fillna(0)
159
158
  # Prepare dataframe
160
159
  df["sum_aum_start"] = df.sum_shares_start * df.price_start
161
- df["sum_aum_end"] = df.sum_shares_end * df.price_end
162
160
 
161
+ df["sum_aum_end"] = df.sum_shares_end * df.price_end
163
162
  # Sanitize dataframe
164
163
  df = df.drop(
165
164
  columns=df.columns.difference(["id", "sum_shares_start", "sum_shares_end", "sum_aum_start", "sum_aum_end"])
@@ -127,7 +127,7 @@ class AssetPositionFilter(wb_filters.FilterSet):
127
127
  )
128
128
 
129
129
  underlying_instrument__is_cash = wb_filters.BooleanFilter(
130
- label="With Cash", exclude=True, lookup_expr="exact", field_name="underlying_instrument__is_cash"
130
+ label="Cash", lookup_expr="exact", method="filter_is_cash"
131
131
  )
132
132
  underlying_instrument__instrument_type = wb_filters.ModelChoiceFilter(
133
133
  label="Instrument Type",
@@ -230,6 +230,13 @@ class AssetPositionFilter(wb_filters.FilterSet):
230
230
  return queryset.filter(underlying_instrument__in=value.get_classified_instruments())
231
231
  return queryset
232
232
 
233
+ def filter_is_cash(self, queryset, name, value):
234
+ if value is True:
235
+ return queryset.filter(Q(underlying_quote__is_cash=True) | Q(underlying_quote__is_cash_equivalent=True))
236
+ if value is False:
237
+ return queryset.filter(Q(underlying_quote__is_cash=False) & Q(underlying_quote__is_cash_equivalent=False))
238
+ return queryset
239
+
233
240
  def filter_portfolio_instrument(self, queryset, name, value):
234
241
  if value:
235
242
  return queryset.filter(
@@ -9,6 +9,7 @@ from django.core.exceptions import ObjectDoesNotExist
9
9
  from django.db import models
10
10
  from wbcore.contrib.authentication.authentication import User
11
11
  from wbcore.contrib.currency.import_export.handlers import CurrencyImportHandler
12
+ from wbcore.contrib.currency.models import Currency
12
13
  from wbcore.contrib.io.exceptions import DeserializationError
13
14
  from wbcore.contrib.io.imports import ImportExportHandler
14
15
  from wbcore.contrib.notifications.dispatch import send_notification
@@ -36,9 +37,13 @@ class AssetPositionImportHandler(ImportExportHandler):
36
37
  portfolio_data = data.pop("portfolio", None)
37
38
  underlying_quote_data = data.pop("underlying_quote", data.pop("underlying_instrument", None))
38
39
  if "currency" in data:
39
- data["currency"] = self.currency_handler.process_object(
40
- data["currency"], read_only=True, raise_exception=False
41
- )[0]
40
+ currency = self.currency_handler.process_object(data["currency"], read_only=True, raise_exception=False)[0]
41
+ # we do not support GBX in our instrument table
42
+ if currency.key == "GBX":
43
+ currency = Currency.objects.get(key="GBP")
44
+ if "initial_price" in data:
45
+ data["initial_price"] /= 1000
46
+ data["currency"] = currency
42
47
  data["date"] = datetime.strptime(data["date"], "%Y-%m-%d").date()
43
48
  if data.get("asset_valuation_date", None):
44
49
  data["asset_valuation_date"] = datetime.strptime(data["asset_valuation_date"], "%Y-%m-%d").date()
@@ -82,7 +82,7 @@ def parse(import_source):
82
82
  cash = (
83
83
  df.loc[
84
84
  cash_mask,
85
- ["currency__key", "initial_currency_fx_rate", "portfolio", "date", "initial_price", "initial_shares"],
85
+ ["currency__key", "initial_currency_fx_rate", "portfolio", "date", "initial_shares"],
86
86
  ]
87
87
  .groupby(
88
88
  [
@@ -94,21 +94,20 @@ def parse(import_source):
94
94
  .agg(
95
95
  {
96
96
  "initial_currency_fx_rate": "mean",
97
- "initial_price": "mean",
98
97
  "initial_shares": "sum",
99
98
  }
100
99
  )
101
100
  .reset_index()
102
101
  ).copy()
103
102
  cash["underlying_quote"] = cash["currency__key"].apply(lambda x: {"currency__key": x, "instrument_type": "cash"})
104
-
103
+ cash["initial_price"] = 1.0
105
104
  # cash_equivalents_mask, all asset type code that match TRES and which don't have accounting category T111
106
105
  cash_equivalents_mask = df["Asset type code"].str.match("TRES") & ~df["Accounting category"].str.match("T111")
107
106
 
108
107
  cash_equivalents = (
109
108
  df.loc[
110
109
  cash_equivalents_mask,
111
- ["currency__key", "initial_currency_fx_rate", "portfolio", "date", "initial_price", "initial_shares"],
110
+ ["currency__key", "initial_currency_fx_rate", "portfolio", "date", "initial_shares"],
112
111
  ]
113
112
  .groupby(
114
113
  [
@@ -120,7 +119,6 @@ def parse(import_source):
120
119
  .agg(
121
120
  {
122
121
  "initial_currency_fx_rate": "mean",
123
- "initial_price": "mean",
124
122
  "initial_shares": "sum",
125
123
  }
126
124
  )
@@ -129,9 +127,11 @@ def parse(import_source):
129
127
  cash_equivalents["underlying_quote"] = cash_equivalents["currency__key"].apply(
130
128
  lambda x: {"currency__key": x, "instrument_type": "cash_equivalent"}
131
129
  )
130
+ cash_equivalents["initial_price"] = 1.0
132
131
 
133
132
  # equities = df.loc[df["Accounting category"].str.match("010"), :].copy() # Historically, we filter out equity base on the "accounting category" that matches "010", we had issue with equity having the code "020". We decided to use the column "Asset type code" and filter out with the code "VCOM"
134
133
  equities = df.loc[df["Asset type code"].str.match("VMOB"), :].copy()
134
+
135
135
  if not equities.empty:
136
136
  equities["underlying_quote"] = equities.apply(lambda x: get_underlying_quote(x), axis=1)
137
137
  equities["exchange"] = equities.apply(lambda x: get_exchange(x), axis=1)
@@ -140,7 +140,6 @@ def parse(import_source):
140
140
  del equities["Bloom Ticker"]
141
141
  del equities["Asset description"]
142
142
  df = pd.concat([cash, equities, cash_equivalents])
143
-
144
143
  df["asset_valuation_date"] = df["date"]
145
144
  # Rename the columns
146
145
 
File without changes
@@ -0,0 +1,85 @@
1
+ from datetime import date, timedelta
2
+
3
+ import pytest
4
+ from pandas._libs.tslibs.offsets import BDay
5
+
6
+ from wbportfolio.analysis.claims import ConsolidatedTradeSummary
7
+ from wbportfolio.models import Claim
8
+
9
+
10
+ @pytest.mark.django_db
11
+ class TestConsolidatedTradeSummary:
12
+ def test_base(self, claim_factory, customer_trade_factory, product, instrument_price_factory):
13
+ d1 = date(2024, 1, 1)
14
+ d2 = date(2025, 1, 1)
15
+ p1 = instrument_price_factory.create(date=d1, instrument=product) # noqa
16
+ p2 = instrument_price_factory.create(date=d2, instrument=product)
17
+ t1 = customer_trade_factory.create(
18
+ shares=1000, transaction_date=d1, underlying_instrument=product, portfolio=product.primary_portfolio
19
+ )
20
+ t2 = customer_trade_factory.create(
21
+ shares=500, transaction_date=d2, underlying_instrument=product, portfolio=product.primary_portfolio
22
+ )
23
+
24
+ c0 = claim_factory.create() # noqa
25
+ c1 = claim_factory.create(shares=1000, status="APPROVED", trade=t1, date=d2, product=product)
26
+ c2 = claim_factory.create(shares=500, status="APPROVED", trade=t2, date=d1, product=product)
27
+ cts = ConsolidatedTradeSummary(Claim.objects.all(), d1, d2, "account", "account__title")
28
+
29
+ valid_date = dict(cts.queryset.values_list("id", "valid_date"))
30
+ assert valid_date == {c1.id: d2, c2.id: d2}
31
+
32
+ date_considered = dict(cts.queryset.values_list("id", "date_considered"))
33
+ assert date_considered == {c1.id: d2 + timedelta(days=1), c2.id: d2 + timedelta(days=1)}
34
+
35
+ aum = dict(cts.queryset.values_list("id", "aum"))
36
+ assert aum == {c1.id: p2.net_value * c1.shares, c2.id: p2.net_value * c2.shares}
37
+
38
+ def test_get_aum_df(self, claim_factory, product, instrument_price_factory, customer_trade_factory):
39
+ t1 = customer_trade_factory.create(
40
+ shares=10,
41
+ transaction_date=date(2024, 12, 31),
42
+ underlying_instrument=product,
43
+ portfolio=product.primary_portfolio,
44
+ )
45
+ t2 = customer_trade_factory.create(
46
+ shares=100,
47
+ transaction_date=date(2025, 2, 3),
48
+ underlying_instrument=product,
49
+ portfolio=product.primary_portfolio,
50
+ )
51
+ c1 = claim_factory.create(trade=t1, status="APPROVED")
52
+ c2 = claim_factory.create(trade=t2, status="APPROVED", account=c1.account)
53
+ d1 = date(2025, 1, 1)
54
+ d2 = date(2025, 3, 3)
55
+ product.prices.all().delete()
56
+ p1 = instrument_price_factory.create(
57
+ date=(d1 - BDay(7)).date(), instrument=product
58
+ ) # we test that the CTS uses the earliest date
59
+ p2 = instrument_price_factory.create(date=c2.date, instrument=product) # noqa
60
+ p3 = instrument_price_factory.create(date=d2, instrument=product)
61
+
62
+ cts = ConsolidatedTradeSummary(Claim.objects.all(), d1, d2, "account", "account__title")
63
+ aum_start = float(round(p1.net_value * c1.shares))
64
+ aum_end = float(round(p3.net_value * (c1.shares + c2.shares)))
65
+ assert cts.get_aum_df().to_dict(orient="index") == {
66
+ c1.account.id: {
67
+ "sum_shares_start": c1.shares,
68
+ "sum_shares_end": c1.shares + c2.shares,
69
+ "sum_aum_start": aum_start,
70
+ "sum_aum_end": aum_end,
71
+ "sum_shares_diff": c2.shares,
72
+ "sum_shares_perf": (c1.shares + c2.shares) / c1.shares - 1,
73
+ "sum_aum_diff": aum_end - aum_start,
74
+ "sum_aum_perf": aum_end / aum_start - 1,
75
+ }
76
+ }
77
+
78
+ p0 = instrument_price_factory.create(date=c1.date, instrument=product) # noqa
79
+
80
+ assert cts.get_nnm_df().to_dict(orient="index") == {
81
+ c1.account.id: {
82
+ "sum_nnm_2025-02": float(round(c2.shares * p2.net_value)),
83
+ "sum_nnm_total": float(round(c2.shares * p2.net_value)),
84
+ }
85
+ }, "Ensure the first claim that is considered in t+1 (on the CTS lower bound range) is excluded from the NNM"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.55.3
3
+ Version: 1.55.5
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*
@@ -28,7 +28,7 @@ wbportfolio/admin/transactions/dividends.py,sha256=2Hx0yvcMp0VvzYfuO_jl-9KFrpq6R
28
28
  wbportfolio/admin/transactions/fees.py,sha256=gpq-_k3ypEGVNuDfFE4OHtn-B9ZtkzA4YYNLtNu-Fc8,1329
29
29
  wbportfolio/admin/transactions/trades.py,sha256=LanpSm6iaR9cmNvSJwpRFPkOgN46K_Y9YoMy0MReYuc,1913
30
30
  wbportfolio/analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
- wbportfolio/analysis/claims.py,sha256=islJVmHg6jKr6Q1b84Vt5Au2bzy9aDjWGEVGNtPFQd4,10688
31
+ wbportfolio/analysis/claims.py,sha256=wcTniEVdDnmME8LHrYbo-WWpQ5EzNPgAT2KJIGQ0IIA,10688
32
32
  wbportfolio/api_clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  wbportfolio/api_clients/ubs.py,sha256=pB_urTr8x8YCW28c_hK7DeCzAx07VeLZ27TamrZln2k,6598
34
34
  wbportfolio/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -82,7 +82,7 @@ wbportfolio/factories/orders/orders.py,sha256=tTw2U-xvUsFpG4zad3KCHZPQx1FVg-zPjw
82
82
  wbportfolio/fdm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
83
  wbportfolio/fdm/tasks.py,sha256=59hXt3TOJaM8KpUaKdBNAGsO7NFMHaAlNA0T3FTUau0,533
84
84
  wbportfolio/filters/__init__.py,sha256=m44CdyYMtku8rMGyiX8KSmMdvyqaElKW4UMkEqOC1VM,1374
85
- wbportfolio/filters/assets.py,sha256=jQoWmK2teQPBpEUNLC3NWl6fhrndl1Tt0sPdQxiPPM4,18538
85
+ wbportfolio/filters/assets.py,sha256=rvBNtQ8OcvYXSmaVdgjH7IITvaBZ558Y9sCj0B_WDpw,18866
86
86
  wbportfolio/filters/assets_and_net_new_money_progression.py,sha256=IYl_qMFO9QKhXhCTtz2JrOjlAKdEkykNlu4DO_H7HkQ,1403
87
87
  wbportfolio/filters/custodians.py,sha256=pStQgPQPhPpnt57_V7BuXbFXmRiZBEAiEeMFuQmt2NE,287
88
88
  wbportfolio/filters/esg.py,sha256=ZEUQh3IQxLSshVrgtPqHC_ToirrllkCXR_KzUlCUtkA,688
@@ -120,7 +120,7 @@ wbportfolio/import_export/backends/wbfdm/dividend.py,sha256=iAQXnYPXmtG_Jrc8THAJ
120
120
  wbportfolio/import_export/backends/wbfdm/mixin.py,sha256=JNtjgqGLson1nu_Chqb8MWyuiF3Ws8ox2vapxIRBYKE,400
121
121
  wbportfolio/import_export/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
122
122
  wbportfolio/import_export/handlers/adjustment.py,sha256=6bdTIYFmc8_HFxcdwtnYwglMyCfAD8XrTIrEb2zWY0g,1757
123
- wbportfolio/import_export/handlers/asset_position.py,sha256=trI_I4IxZ6fqV9_BpRAvPNkYf07-loQB7f4ipkGkJbE,8695
123
+ wbportfolio/import_export/handlers/asset_position.py,sha256=EWBmuZActTmBk4PJRjVRby-99g27LH6cWLbWve9N8GM,9000
124
124
  wbportfolio/import_export/handlers/dividend.py,sha256=F0oLfNt2B_QQAjHBCRpxa5HSkfkAYdal_NjLJGtVckY,4408
125
125
  wbportfolio/import_export/handlers/fees.py,sha256=BOFHAvSTlvVLaxnm6KD_fcza1TlPc02HOR9J0_jjswI,2495
126
126
  wbportfolio/import_export/handlers/orders.py,sha256=GU3_tIy-tAw9aU-ifsnmMZPBB9sqfkFC_S1d9VziTwg,3136
@@ -161,7 +161,7 @@ wbportfolio/import_export/parsers/sg_lux/customer_trade.py,sha256=gTEUIaxlZvXXCg
161
161
  wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py,sha256=WyRpKXkNhpK4H9aNjW7_ggcCGlN_-9ETlR1QsE9xl_k,5068
162
162
  wbportfolio/import_export/parsers/sg_lux/customer_trade_slk.py,sha256=D_1c0v6lTXkopjZzr9pQb575HxrMFe91tKk8xT1X3Ns,3604
163
163
  wbportfolio/import_export/parsers/sg_lux/customer_trade_without_pw.py,sha256=nMYV9-OwY_TagU36Ab0dOweZYzVriFWHXPYjauBg2Yk,2034
164
- wbportfolio/import_export/parsers/sg_lux/equity.py,sha256=SXupaP9W299oFfnucV7FogY_yqusLkbqQFR3nGt-4m0,6117
164
+ wbportfolio/import_export/parsers/sg_lux/equity.py,sha256=IRwjeQmNFGuALUCWpWXWtflUEjl5qKyJumlQIAHmOPk,6076
165
165
  wbportfolio/import_export/parsers/sg_lux/fees.py,sha256=AK2TGOjI3JrOjtACEQd-4ffuedtIus-_gWug6wN6RQs,1846
166
166
  wbportfolio/import_export/parsers/sg_lux/perf_fees.py,sha256=6HAV3Pu2aaYARjMQvN24Op2rru_YdjeI9J9ADDQVH8s,1513
167
167
  wbportfolio/import_export/parsers/sg_lux/portfolio_cash_flow.py,sha256=ftLKWukUl9AcGWHQmJZnShVZpkO4rOl04PGlLcalDdQ,889
@@ -399,6 +399,8 @@ wbportfolio/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
399
399
  wbportfolio/tests/conftest.py,sha256=007ydJK59J1guakXWYrZ-waDZNgXAXwMYSJ-BTDFVhA,4614
400
400
  wbportfolio/tests/signals.py,sha256=aUu4nDWmVAAWyokcWJDj2RsajeIa070qXsZeNq6ynqY,5545
401
401
  wbportfolio/tests/tests.py,sha256=3NdpJQlksLclF-7lVP07WJXaEzquhneoDX1lIA_tyXU,1121
402
+ wbportfolio/tests/analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
403
+ wbportfolio/tests/analysis/test_claims.py,sha256=aWiYASn7sLg255Bq38kUd9skpYQKxnROUKau6ha4uww,4036
402
404
  wbportfolio/tests/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
403
405
  wbportfolio/tests/models/test_account_reconciliation.py,sha256=7IDZh6qbG7-3czbNf5aDRaEDnMBw2Qiqk_h425dFu9w,8569
404
406
  wbportfolio/tests/models/test_assets.py,sha256=N8OP2ZTPdq0lGonFRB0q_ruwF8QYpOGLkPWoucr8u_E,10428
@@ -565,7 +567,7 @@ wbportfolio/viewsets/transactions/claim.py,sha256=Pb1WftoO-w-ZSTbLRhmQubhy7hgd68
565
567
  wbportfolio/viewsets/transactions/fees.py,sha256=WT2bWWfgozz4_rpyTKX7dgBBTXD-gu0nlsd2Nk2Zh1Q,7028
566
568
  wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
567
569
  wbportfolio/viewsets/transactions/trades.py,sha256=xBgOGaJ8aEg-2RxEJ4FDaBs4SGwuLasun3nhpis0WQY,12363
568
- wbportfolio-1.55.3.dist-info/METADATA,sha256=LE4MTUvlSpHHJ82XECeFV1B3sI56oqaK4M_Fk7rkYGw,751
569
- wbportfolio-1.55.3.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
570
- wbportfolio-1.55.3.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
571
- wbportfolio-1.55.3.dist-info/RECORD,,
570
+ wbportfolio-1.55.5.dist-info/METADATA,sha256=BSYTmVfS-XgxIctFW8n7654BngmPS2PnUP8UwhUZssg,751
571
+ wbportfolio-1.55.5.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
572
+ wbportfolio-1.55.5.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
573
+ wbportfolio-1.55.5.dist-info/RECORD,,