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

@@ -3,7 +3,8 @@ from typing import TYPE_CHECKING
3
3
  import numpy as np
4
4
  import pandas as pd
5
5
  from django.db import connection
6
- from django.db.models import DecimalField, ExpressionWrapper, F, OuterRef, Subquery
6
+ from django.db.models import DecimalField, ExpressionWrapper, F, OuterRef, Subquery, DateField
7
+ from django.db.models.functions import Greatest
7
8
  from django.template.loader import get_template
8
9
  from jinjasql import JinjaSql
9
10
  from wbcore.contrib.currency.models import CurrencyFXRates
@@ -47,6 +48,7 @@ class ConsolidatedTradeSummary:
47
48
  pivot_label: str,
48
49
  classification_group: ClassificationGroup | None = None,
49
50
  classification_height: int = 0,
51
+ date_label: str = "date_considered",
50
52
  ):
51
53
  self.queryset = queryset.filter(trade__isnull=False, status="APPROVED")
52
54
  self.start_date = start_date
@@ -60,6 +62,9 @@ class ConsolidatedTradeSummary:
60
62
  fx_rate=CurrencyFXRates.get_fx_rates_subquery("date", lookup_expr="exact"),
61
63
  aum=ExpressionWrapper(F("fx_rate") * F("net_value") * F("shares"), output_field=DecimalField()),
62
64
  internal_trade=F("trade__marked_as_internal"),
65
+ date_considered=ExpressionWrapper(
66
+ Greatest("trade__transaction_date", "date") + 1, output_field=DateField()
67
+ ),
63
68
  )
64
69
  self.queryset = Account.annotate_root_account_info(self.queryset)
65
70
 
@@ -71,17 +76,17 @@ class ConsolidatedTradeSummary:
71
76
  "account",
72
77
  "product__parent",
73
78
  )
74
- self.df = self._prepare_df()
79
+ self.df = self._prepare_df(date_label)
75
80
 
76
- def _prepare_df(self) -> pd.DataFrame:
77
- columns = ["shares", "date", "product__id", "aum", "internal_trade"]
81
+ def _prepare_df(self, date_label: str) -> pd.DataFrame:
82
+ columns = ["shares", date_label, "product__id", "aum", "internal_trade"]
78
83
  if self.pivot == "classification_id":
79
84
  columns.append("classifications")
80
85
  else:
81
86
  columns.append(self.pivot)
82
87
  columns.append(self.pivot_label)
83
88
 
84
- df = pd.DataFrame(self.queryset.values_list(*columns), columns=columns)
89
+ df = pd.DataFrame(self.queryset.values_list(*columns), columns=columns).rename(columns={date_label: "date"})
85
90
  if self.pivot == "classification_id":
86
91
  df = (
87
92
  df.explode("classifications")
@@ -112,7 +117,7 @@ class ConsolidatedTradeSummary:
112
117
  "net_value",
113
118
  date_name="net_value_date_start",
114
119
  instrument_pk_name="pk",
115
- date_lookup="exact"
120
+ date_lookup="exact",
116
121
  # we get all net value (even estimated) to avoir showing None price on holiday
117
122
  ),
118
123
  fx_rate_start=CurrencyFXRates.get_fx_rates_subquery(
@@ -123,7 +128,7 @@ class ConsolidatedTradeSummary:
123
128
  "net_value",
124
129
  date_name="net_value_date_end",
125
130
  instrument_pk_name="pk",
126
- date_lookup="exact"
131
+ date_lookup="exact",
127
132
  # we get all net value (even estimated) to avoir showing None price on holiday
128
133
  ),
129
134
  fx_rate_end=CurrencyFXRates.get_fx_rates_subquery(
@@ -22,7 +22,7 @@ class BaseProductFilterSet(InstrumentFilterSet):
22
22
  label="Bank",
23
23
  queryset=Company.objects.all(),
24
24
  endpoint=Company.get_representation_endpoint(),
25
- filter_params={"bank_product": True},
25
+ filter_params={"notnull_related_name": "issues_products"},
26
26
  value_key=Company.get_representation_value_key(),
27
27
  label_key=Company.get_representation_label_key(),
28
28
  )
@@ -3,32 +3,12 @@ from datetime import date
3
3
  from django.db.models import Exists, OuterRef
4
4
  from django.dispatch import receiver
5
5
  from wbcore import filters as wb_filters
6
- from wbcore.contrib.directory.filters import CompanyFilter
7
6
  from wbcore.signals.filters import add_filters
8
7
  from wbfdm.filters import BaseClassifiedInstrumentFilterSet, ClassificationFilter
9
8
  from wbfdm.models import InstrumentClassificationThroughModel
10
9
  from wbportfolio.models import AssetPosition, Portfolio
11
10
 
12
11
 
13
- @receiver(add_filters, sender=CompanyFilter)
14
- def add_bank_product_filter(sender, request=None, *args, **kwargs):
15
- def method_bank(queryset, name, value):
16
- if value is True:
17
- return queryset.filter(issues_products__isnull=False).distinct()
18
- elif value is False:
19
- return queryset.filter(issues_products__isnull=True).distinct()
20
- return queryset
21
-
22
- return {
23
- "bank_product": wb_filters.BooleanFilter(
24
- field_name="bank_product",
25
- label="Is a Bank",
26
- help_text="Filter for companies that are a bank and serve as the custodian of a product in the PMS",
27
- method=method_bank,
28
- )
29
- }
30
-
31
-
32
12
  @receiver(add_filters, sender=ClassificationFilter)
33
13
  def add_portfolio_filter(sender, request=None, *args, **kwargs):
34
14
  def _filter_portfolio(queryset, name, value):
@@ -3,6 +3,7 @@ from .claim import (
3
3
  ClaimGroupByFilter,
4
4
  ConsolidatedTradeSummaryTableFilterSet,
5
5
  CumulativeNNMChartFilter,
6
+ DistributionNNMChartFilter,
6
7
  CustomerAPIFilter,
7
8
  CustomerClaimFilter,
8
9
  CustomerClaimGroupByFilter,
@@ -82,7 +82,6 @@ class CommissionBaseFilterSet(wb_filters.FilterSet):
82
82
  endpoint=Account.get_representation_endpoint(),
83
83
  value_key=Account.get_representation_value_key(),
84
84
  label_key=Account.get_representation_label_key(),
85
- filter_params={"status": "OPEN"},
86
85
  )
87
86
 
88
87
  manager_role = wb_filters.ModelChoiceFilter(
@@ -205,7 +204,7 @@ class ClaimFilter(OppositeSharesFieldMethodMixin, CommissionBaseFilterSet):
205
204
  }
206
205
 
207
206
 
208
- class ClaimGroupByFilter(ClaimFilter):
207
+ class ClaimGroupByFilter(CommissionBaseFilterSet):
209
208
  pending_approval = in_charge_of_customer = linked_trade = None
210
209
 
211
210
  only_new_customer = wb_filters.BooleanFilter(method="filter_only_new_customer", label="Only new customers")
@@ -303,6 +302,19 @@ class ConsolidatedTradeSummaryTableFilterSet(PandasFilterSetMixin, ClaimGroupByF
303
302
  }
304
303
 
305
304
 
305
+ class DistributionNNMChartFilter(ClaimGroupByFilter):
306
+ percent = wb_filters.BooleanFilter(
307
+ method="fake_filter",
308
+ default=False,
309
+ help_text="True if the value are displayed in percentage of the initial total AUM",
310
+ label="Show percentage",
311
+ )
312
+
313
+ class Meta:
314
+ model = Claim
315
+ fields = {}
316
+
317
+
306
318
  class CumulativeNNMChartFilter(ClaimGroupByFilter):
307
319
  groupby_classification_group = group_by = None
308
320
 
@@ -0,0 +1,19 @@
1
+ # Generated by Django 5.0.10 on 2025-01-28 10:17
2
+
3
+ import django.db.models.expressions
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('wbportfolio', '0071_alter_trade_options_alter_trade_order'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name='trade',
16
+ name='diff_shares',
17
+ field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('shares'), '-', models.F('claimed_shares')), output_field=models.DecimalField(decimal_places=4, max_digits=15)),
18
+ ),
19
+ ]
@@ -416,12 +416,15 @@ class AssetPosition(ImportMixin, models.Model):
416
416
  net_value *= self.currency.convert(
417
417
  self.asset_valuation_date, self.underlying_instrument.currency
418
418
  )
419
- self.underlying_instrument_price = InstrumentPrice.objects.create(
419
+ self.underlying_instrument_price = InstrumentPrice(
420
420
  calculated=False,
421
421
  instrument=self.underlying_instrument,
422
422
  date=self.asset_valuation_date,
423
423
  net_value=net_value,
424
+ import_source=self.import_source, # we set the import source to know where this price is coming from
424
425
  )
426
+ self.underlying_instrument_price.fill_market_capitalization()
427
+ self.underlying_instrument_price.save()
425
428
  else: # sometime, the asset valuation date does not correspond to a valid market date. In that case, we get the latest valid instrument price for that product
426
429
  self.underlying_instrument_price = (
427
430
  InstrumentPrice.objects.filter(
@@ -25,6 +25,7 @@ from django.db.models.signals import post_save
25
25
  from django.dispatch import receiver
26
26
  from psycopg.types.range import DateRange
27
27
  from wbcore.contrib.currency.models import CurrencyFXRates
28
+ from wbcore.contrib.notifications.utils import create_notification_type
28
29
  from wbcore.models import WBModel
29
30
  from wbcore.utils.models import ActiveObjectManager, DeleteToDisableMixin
30
31
  from wbfdm.contrib.metric.dispatch import compute_metrics
@@ -232,7 +233,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
232
233
  verbose_name_plural = "Portfolios"
233
234
 
234
235
  notification_types = [
235
- (
236
+ create_notification_type(
236
237
  "wbportfolio.portfolio.check_custodian_portfolio",
237
238
  "Check Custodian Portfolio",
238
239
  "Sends a notification when a portfolio does not match with its custodian portfolio",
@@ -3,6 +3,7 @@ from decimal import Decimal
3
3
 
4
4
  from django.db import models
5
5
  from wbcore.contrib.io.mixins import ImportMixin
6
+ from wbcore.contrib.notifications.utils import create_notification_type
6
7
  from wbcore.models import WBModel
7
8
  from wbportfolio.import_export.handlers.portfolio_cash_flow import (
8
9
  DailyPortfolioCashFlowImportHandler,
@@ -132,7 +133,7 @@ class DailyPortfolioCashFlow(ImportMixin, WBModel):
132
133
  ]
133
134
  permissions = [("administrate_dailyportfoliocashflow", "Can administrate Daily Portfolio CashFlow")]
134
135
  notification_types = [
135
- (
136
+ create_notification_type(
136
137
  "wbportfolio.dailyportfoliocashflow.notify_rebalance",
137
138
  "Rebalancing suggested",
138
139
  "Sends a notification, when the system suggests to rebalance a portfolio due to being outside of the cash target parameters",
@@ -140,7 +141,7 @@ class DailyPortfolioCashFlow(ImportMixin, WBModel):
140
141
  True,
141
142
  False,
142
143
  ),
143
- (
144
+ create_notification_type(
144
145
  "wbportfolio.dailyportfoliocashflow.notify_swingpricing",
145
146
  "Swing Pricing Notification",
146
147
  "Sends a notification, when the system detects a future swing pricing event",
@@ -21,7 +21,7 @@ from django.db.models.signals import post_save
21
21
  from django.dispatch import receiver
22
22
  from django.utils.functional import cached_property
23
23
  from django_fsm import GET_STATE, FSMField, transition
24
- from ordered_model.models import OrderedModel, OrderedModelManager
24
+ from ordered_model.models import OrderedModel, OrderedModelManager, OrderedModelQuerySet
25
25
  from wbcore.contrib.icons import WBIcon
26
26
  from wbcore.enums import RequestType
27
27
  from wbcore.metadata.configs.buttons import ActionButton
@@ -37,58 +37,65 @@ from wbportfolio.pms.typing import Trade as TradeDTO
37
37
  from .transactions import ShareMixin, Transaction
38
38
 
39
39
 
40
- class DefaultTradeManager(OrderedModelManager):
41
- """This manager is expect to be the trade default manager and annotate by default the effective weight (extracted
42
- from the associated portfolio) and the target weight as an addition between the effective weight and the delta weight
43
- """
44
-
45
- def get_queryset(self):
46
- return (
47
- super()
48
- .get_queryset()
49
- .annotate(
50
- last_effective_date=Subquery(
40
+ class TradeQueryset(OrderedModelQuerySet):
41
+ def annotate_base_info(self):
42
+ return self.annotate(
43
+ last_effective_date=Subquery(
44
+ AssetPosition.objects.filter(
45
+ underlying_instrument=OuterRef("underlying_instrument"),
46
+ date__lt=OuterRef("transaction_date"),
47
+ portfolio=OuterRef("portfolio"),
48
+ )
49
+ .order_by("-date")
50
+ .values("date")[:1]
51
+ ),
52
+ effective_weight=Coalesce(
53
+ Subquery(
51
54
  AssetPosition.objects.filter(
52
55
  underlying_instrument=OuterRef("underlying_instrument"),
53
- date__lt=OuterRef("transaction_date"),
56
+ date=OuterRef("last_effective_date"),
54
57
  portfolio=OuterRef("portfolio"),
55
58
  )
56
- .order_by("-date")
57
- .values("date")[:1]
58
- ),
59
- effective_weight=Coalesce(
60
- Subquery(
61
- AssetPosition.objects.filter(
62
- underlying_instrument=OuterRef("underlying_instrument"),
63
- date=OuterRef("last_effective_date"),
64
- portfolio=OuterRef("portfolio"),
65
- )
66
- .values("portfolio")
67
- .annotate(s=Sum("weighting"))
68
- .values("s")[:1]
69
- ),
70
- Decimal(0),
59
+ .values("portfolio")
60
+ .annotate(s=Sum("weighting"))
61
+ .values("s")[:1]
71
62
  ),
72
- target_weight=F("effective_weight") + F("weighting"),
73
- effective_shares=Coalesce(
74
- Subquery(
75
- AssetPosition.objects.filter(
76
- underlying_instrument=OuterRef("underlying_instrument"),
77
- date=OuterRef("last_effective_date"),
78
- portfolio=OuterRef("portfolio"),
79
- )
80
- .values("portfolio")
81
- .annotate(s=Sum("shares"))
82
- .values("s")[:1]
83
- ),
84
- Decimal(0),
63
+ Decimal(0),
64
+ ),
65
+ target_weight=F("effective_weight") + F("weighting"),
66
+ effective_shares=Coalesce(
67
+ Subquery(
68
+ AssetPosition.objects.filter(
69
+ underlying_instrument=OuterRef("underlying_instrument"),
70
+ date=OuterRef("last_effective_date"),
71
+ portfolio=OuterRef("portfolio"),
72
+ )
73
+ .values("portfolio")
74
+ .annotate(s=Sum("shares"))
75
+ .values("s")[:1]
85
76
  ),
86
- target_shares=F("effective_weight") * F("weighting"),
87
- diff_shares=F("shares") - F("claimed_shares"),
88
- )
77
+ Decimal(0),
78
+ ),
79
+ target_shares=F("effective_weight") * F("weighting"),
89
80
  )
90
81
 
91
82
 
83
+ class DefaultTradeManager(OrderedModelManager):
84
+ """This manager is expect to be the trade default manager and annotate by default the effective weight (extracted
85
+ from the associated portfolio) and the target weight as an addition between the effective weight and the delta weight
86
+ """
87
+
88
+ def __init__(self, with_annotation: bool = False, *args, **kwargs):
89
+ self.with_annotation = with_annotation
90
+ super().__init__(*args, **kwargs)
91
+
92
+ def get_queryset(self) -> TradeQueryset:
93
+ qs = TradeQueryset(self.model, using=self._db)
94
+ if self.with_annotation:
95
+ qs = qs.annotate_base_info()
96
+ return qs
97
+
98
+
92
99
  class ValidCustomerTradeManager(DefaultTradeManager):
93
100
  def __init__(self, without_internal_trade: bool = False):
94
101
  self.without_internal_trade = without_internal_trade
@@ -202,7 +209,13 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
202
209
  help_text="The number of shares that were claimed.",
203
210
  verbose_name="Claimed Shares",
204
211
  )
212
+ diff_shares = models.GeneratedField(
213
+ expression=F("shares") - F("claimed_shares"),
214
+ output_field=models.DecimalField(max_digits=15, decimal_places=4),
215
+ db_persist=True,
216
+ )
205
217
  objects = DefaultTradeManager()
218
+ annotated_objects = DefaultTradeManager(with_annotation=True)
206
219
  valid_customer_trade_objects = ValidCustomerTradeManager()
207
220
  valid_external_customer_trade_objects = ValidCustomerTradeManager(without_internal_trade=True)
208
221
 
@@ -436,13 +449,6 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
436
449
  def _target_shares(self) -> Decimal:
437
450
  return getattr(self, "target_shares", self._effective_shares * self.weighting)
438
451
 
439
- @cached_property
440
- @admin.display(description="Diff Claims")
441
- def _diff_shares(self) -> Decimal:
442
- if hasattr(self, "diff_shares"):
443
- return self.diff_shares
444
- return self.shares - self.claimed_shares
445
-
446
452
  order_with_respect_to = "trade_proposal"
447
453
 
448
454
  class Meta(OrderedModel.Meta):
@@ -51,46 +51,58 @@ class TestAccountRuleModel:
51
51
  account = account_factory.create(owner=entry)
52
52
  other_account = account_factory.create()
53
53
  claim_factory.create(
54
- date=(weekday - BDay(business_days_interval + 2)).date(),
54
+ date=(weekday - BDay(business_days_interval + 3)).date(),
55
55
  account=account,
56
- trade=customer_trade_factory.create(underlying_instrument=product),
56
+ trade=customer_trade_factory.create(
57
+ underlying_instrument=product, transaction_date=(weekday - BDay(business_days_interval + 3)).date()
58
+ ),
57
59
  status="APPROVED",
58
60
  shares=100,
59
61
  )
60
62
  claim_factory.create(
61
- date=(weekday - BDay(business_days_interval + 1)).date(),
63
+ date=(weekday - BDay(business_days_interval + 2)).date(),
62
64
  account=account,
63
- trade=customer_trade_factory.create(underlying_instrument=product),
65
+ trade=customer_trade_factory.create(
66
+ underlying_instrument=product, transaction_date=(weekday - BDay(business_days_interval + 2)).date()
67
+ ),
64
68
  status="APPROVED",
65
69
  shares=-50,
66
70
  ) # this drop should not be detected
67
71
 
68
72
  claim_factory.create(
69
- date=(weekday - BDay(business_days_interval)).date(),
73
+ date=(weekday - BDay(business_days_interval + 1)).date(),
70
74
  account=account,
71
- trade=customer_trade_factory.create(underlying_instrument=product),
75
+ trade=customer_trade_factory.create(
76
+ underlying_instrument=product, transaction_date=(weekday - BDay(business_days_interval + 1)).date()
77
+ ),
72
78
  status="APPROVED",
73
79
  shares=150,
74
80
  )
75
81
  claim_factory.create(
76
- date=weekday,
82
+ date=(weekday - BDay(1)).date(),
77
83
  account=account,
78
- trade=customer_trade_factory.create(underlying_instrument=product),
84
+ trade=customer_trade_factory.create(
85
+ underlying_instrument=product, transaction_date=(weekday - BDay(1)).date()
86
+ ),
79
87
  status="APPROVED",
80
88
  shares=-50,
81
89
  ) # this drop should be detected
82
90
 
83
91
  claim_factory.create(
84
- date=(weekday - BDay(business_days_interval)).date(),
92
+ date=(weekday - BDay(business_days_interval + 1)).date(),
85
93
  account=other_account,
86
- trade=customer_trade_factory.create(underlying_instrument=product),
94
+ trade=customer_trade_factory.create(
95
+ underlying_instrument=product, transaction_date=(weekday - BDay(business_days_interval + 1)).date()
96
+ ),
87
97
  status="APPROVED",
88
98
  shares=150,
89
99
  )
90
100
  claim_factory.create(
91
- date=weekday,
101
+ date=(weekday - BDay(1)).date(),
92
102
  account=other_account,
93
- trade=customer_trade_factory.create(underlying_instrument=product),
103
+ trade=customer_trade_factory.create(
104
+ underlying_instrument=product, transaction_date=(weekday - BDay(1)).date()
105
+ ),
94
106
  status="APPROVED",
95
107
  shares=-50,
96
108
  ) # this drop is valid but an another account so won't be detected
@@ -45,11 +45,7 @@ class TradeProposalRepresentationSerializer(wb_serializers.RepresentationSeriali
45
45
 
46
46
  class TradeRepresentationSerializer(TransactionRepresentationSerializer):
47
47
  _detail = wb_serializers.HyperlinkField(reverse_name="wbportfolio:trade-detail")
48
-
49
- diff_shares = wb_serializers.SerializerMethodField()
50
-
51
- def get_diff_shares(self, obj):
52
- return obj._diff_shares
48
+ diff_shares = wb_serializers.DecimalField(max_digits=15, decimal_places=4)
53
49
 
54
50
  class Meta:
55
51
  model = Trade
@@ -241,7 +237,7 @@ class TradeTradeProposalModelSerializer(TradeModelSerializer):
241
237
  percent_fields = ["effective_weight", "target_weight", "weighting"]
242
238
  read_only_fields = (
243
239
  "transaction_subtype",
244
- "shares"
240
+ "shares",
245
241
  # "underlying_instrument",
246
242
  # "_underlying_instrument",
247
243
  )
@@ -46,7 +46,6 @@ from wbfdm.preferences import get_default_classification_group
46
46
  from wbportfolio.analysis.claims import ConsolidatedTradeSummary
47
47
  from wbportfolio.filters import (
48
48
  ClaimFilter,
49
- ClaimGroupByFilter,
50
49
  ConsolidatedTradeSummaryTableFilterSet,
51
50
  CumulativeNNMChartFilter,
52
51
  CustomerAPIFilter,
@@ -54,6 +53,7 @@ from wbportfolio.filters import (
54
53
  CustomerClaimGroupByFilter,
55
54
  NegativeTermimalAccountPerProductFilterSet,
56
55
  ProfitAndLossPandasFilter,
56
+ DistributionNNMChartFilter,
57
57
  )
58
58
  from wbportfolio.models import Product, Trade
59
59
  from wbportfolio.models.transactions.claim import Claim, ClaimGroupbyChoice
@@ -607,10 +607,18 @@ class ConsolidatedTradeSummaryTableView(ClaimPermissionMixin, ExportPandasAPIVie
607
607
  def get_filterset_class(self, request):
608
608
  profile = request.user.profile
609
609
  if profile.is_internal or request.user.is_superuser:
610
- return ClaimGroupByFilter
610
+ return ConsolidatedTradeSummaryTableFilterSet
611
611
  return CustomerClaimGroupByFilter
612
612
 
613
613
 
614
+ def _sanitize_df(df: pd.DataFrame, normalization_factor: pd.Series | None = None) -> pd.DataFrame:
615
+ df = df.replace({0: np.nan})
616
+ scalar_columns = df.columns.difference(["title", "id"])
617
+ if normalization_factor is not None:
618
+ df[scalar_columns] = df[scalar_columns].div(normalization_factor.replace({0: np.nan}), axis=0)
619
+ return df.dropna(axis=0, how="all", subset=scalar_columns)
620
+
621
+
614
622
  class ConsolidatedTradeSummaryDistributionChart(ConsolidatedTradeSummaryTableView):
615
623
  WIDGET_TYPE = WidgetType.CHART.value
616
624
  IDENTIFIER = "wbportfolio:consolidatedtradesummarydistributionchart"
@@ -618,7 +626,13 @@ class ConsolidatedTradeSummaryDistributionChart(ConsolidatedTradeSummaryTableVie
618
626
  title_config_class = ConsolidatedTradeSummaryDistributionChartTitleConfig
619
627
  endpoint_config_class = ConsolidatedTradeSummaryDistributionChartEndpointConfig
620
628
  button_config_class = None
621
- filterset_class = ClaimGroupByFilter
629
+
630
+ def get_filterset_class(self, request):
631
+ return DistributionNNMChartFilter
632
+
633
+ @cached_property
634
+ def is_percent(self) -> bool:
635
+ return self.request.GET.get("percent", "false") == "true"
622
636
 
623
637
  # TODO This is not really optimal. We need to change it at some point
624
638
  def list(self, request, *args, **kwargs):
@@ -642,36 +656,46 @@ class ConsolidatedTradeSummaryDistributionChart(ConsolidatedTradeSummaryTableVie
642
656
  def get_plotly(self, df):
643
657
  fig = go.Figure()
644
658
  # 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:
659
+ df = df.sort_values(by="title").set_index("id")
660
+ normalization_factor = None
661
+ if self.is_percent:
662
+ normalization_factor = df["sum_aum_start"]
663
+ nnm_monthly_columns = dict(self.nnm_monthly_columns)
664
+ df = _sanitize_df(
665
+ df.drop(columns=df.columns.difference(["title", *nnm_monthly_columns.keys(), "sum_nnm_total"])),
666
+ normalization_factor=normalization_factor,
667
+ )
668
+ for key, label in nnm_monthly_columns.items():
669
+ if len(nnm_monthly_columns.keys()) == 1 and hasattr(self, "df_nnm_neg") and hasattr(self, "df_nnm_pos"):
670
+ df_nnm_pos = _sanitize_df(self.df_nnm_pos.set_index("id"), normalization_factor)
671
+ df_nnm_neg = _sanitize_df(self.df_nnm_neg.set_index("id"), normalization_factor)
672
+ if key in df_nnm_neg.columns:
649
673
  fig.add_trace(
650
674
  go.Histogram(
651
675
  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)",
676
+ y=df_nnm_neg[key],
677
+ x=df_nnm_neg["title"],
678
+ name=label + " (Negative)",
655
679
  marker_color="#FF6961",
656
680
  )
657
681
  )
658
- if nnm_monthly_colum[0] in self.df_nnm_pos.columns:
682
+ if key in df_nnm_pos.columns:
659
683
  fig.add_trace(
660
684
  go.Histogram(
661
685
  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)",
686
+ y=df_nnm_pos[key],
687
+ x=df_nnm_pos["title"],
688
+ name=label + " (Positive)",
665
689
  marker_color="#77DD77",
666
690
  )
667
691
  )
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:
692
+ if key in df.columns:
693
+ figure_kwargs = {"name": label}
694
+ if len(nnm_monthly_columns.keys()) == 1:
671
695
  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:
696
+ figure_kwargs["name"] = label + " (Total)"
697
+ fig.add_trace(go.Histogram(histfunc="sum", y=df[key], x=df["title"], **figure_kwargs))
698
+ if len(nnm_monthly_columns.keys()) > 1:
675
699
  fig.add_trace(
676
700
  go.Histogram(
677
701
  histfunc="sum",
@@ -692,6 +716,8 @@ class ConsolidatedTradeSummaryDistributionChart(ConsolidatedTradeSummaryTableVie
692
716
  "x": 0.5,
693
717
  },
694
718
  )
719
+ if self.is_percent:
720
+ fig.update_yaxes(tickformat=".2%")
695
721
  return fig
696
722
 
697
723
 
@@ -3,12 +3,24 @@ from decimal import Decimal
3
3
  import pandas as pd
4
4
  import plotly.graph_objects as go
5
5
  from django.contrib.messages import warning
6
- from django.db.models import BooleanField, Case, F, OuterRef, Subquery, Sum, Value, When
6
+ from django.db.models import (
7
+ BooleanField,
8
+ Case,
9
+ F,
10
+ OuterRef,
11
+ Subquery,
12
+ Sum,
13
+ Value,
14
+ When,
15
+ DecimalField,
16
+ ExpressionWrapper,
17
+ )
7
18
  from django.db.models.functions import Coalesce
8
19
  from django.shortcuts import get_object_or_404
9
20
  from django.utils.functional import cached_property
10
21
  from django_filters.rest_framework import DjangoFilterBackend
11
22
  from wbcore import viewsets
23
+ from wbcore.contrib.currency.models import CurrencyFXRates
12
24
  from wbcore.permissions.permissions import InternalUserPermissionMixin
13
25
  from wbcore.utils.strings import format_number
14
26
  from wbcrm.models import Account
@@ -55,7 +67,6 @@ from ..configs import (
55
67
  TradeTradeProposalEndpointConfig,
56
68
  )
57
69
  from ..mixins import UserPortfolioRequestPermissionMixin
58
- from .transactions import TransactionModelViewSet
59
70
 
60
71
 
61
72
  class TradeProposalRepresentationViewSet(InternalUserPermissionMixin, viewsets.RepresentationViewSet):
@@ -93,7 +104,7 @@ class TradeRepresentationViewSet(InternalUserPermissionMixin, viewsets.Represent
93
104
  )
94
105
 
95
106
 
96
- class TradeModelViewSet(TransactionModelViewSet):
107
+ class TradeModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserPermissionMixin, viewsets.ModelViewSet):
97
108
  IDENTIFIER = "wbportfolio:trade"
98
109
 
99
110
  ordering_fields = (
@@ -141,7 +152,9 @@ class TradeModelViewSet(TransactionModelViewSet):
141
152
  "shares": {
142
153
  "Σ": format_number(queryset.aggregate(s=Sum("shares"))["s"] or Decimal(0)),
143
154
  },
144
- **super().get_aggregates(queryset, paginated_queryset),
155
+ "total_value_usd": {
156
+ "Σ": format_number(queryset.aggregate(s=Sum("total_value_usd"))["s"] or Decimal(0)),
157
+ },
145
158
  }
146
159
 
147
160
  def get_queryset(self):
@@ -150,6 +163,11 @@ class TradeModelViewSet(TransactionModelViewSet):
150
163
  qs = qs.filter(transaction_subtype__in=[Trade.Type.REDEMPTION, Trade.Type.SUBSCRIPTION])
151
164
  qs = (
152
165
  qs.annotate(
166
+ fx_rate=CurrencyFXRates.get_fx_rates_subquery(
167
+ "transaction_date", currency="currency", lookup_expr="exact"
168
+ ), # this slow down the request. An alternative would be to store the value in the model.
169
+ total_value_usd=ExpressionWrapper(F("total_value"), output_field=DecimalField()),
170
+ total_value_gross_usd=ExpressionWrapper(F("total_value_gross"), output_field=DecimalField()),
153
171
  approved_claimed_shares=Coalesce(
154
172
  Subquery(
155
173
  Claim.objects.filter(status=Claim.Status.APPROVED, trade=OuterRef("pk"))
@@ -391,5 +409,5 @@ class TradeTradeProposalModelViewSet(
391
409
 
392
410
  def get_queryset(self):
393
411
  if self.is_portfolio_manager:
394
- return super().get_queryset().filter(trade_proposal=self.kwargs["trade_proposal_id"])
412
+ return super().get_queryset().filter(trade_proposal=self.kwargs["trade_proposal_id"]).annotate_base_info()
395
413
  return TradeProposal.objects.none()
@@ -77,10 +77,8 @@ class TransactionModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserP
77
77
  fx_rate=CurrencyFXRates.get_fx_rates_subquery(
78
78
  "transaction_date", currency="currency", lookup_expr="exact"
79
79
  ),
80
- total_value_usd=ExpressionWrapper(F("total_value") * F("fx_rate"), output_field=DecimalField()),
81
- total_value_gross_usd=ExpressionWrapper(
82
- F("total_value_gross") * F("fx_rate"), output_field=DecimalField()
83
- ),
80
+ total_value_usd=ExpressionWrapper(F("total_value"), output_field=DecimalField()),
81
+ total_value_gross_usd=ExpressionWrapper(F("total_value_gross"), output_field=DecimalField()),
84
82
  transaction_underlying_type_trade=Subquery(
85
83
  Trade.objects.filter(id=OuterRef("pk")).values("transaction_subtype")[:1]
86
84
  ),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.44.2
3
+ Version: 1.44.4
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*
@@ -27,7 +27,7 @@ wbportfolio/admin/transactions/fees.py,sha256=_r0nPN9d2UM0Ef4axmh0GMZW13HlOCW9Ty
27
27
  wbportfolio/admin/transactions/trades.py,sha256=Di_dq6wCUy2ZwOBTCOVckzNsTUGBb4wvdcYANK5yMVI,1728
28
28
  wbportfolio/admin/transactions/transactions.py,sha256=nsUjfZ5wDCD6vdj4aBgYo8h9ZtyZ3HC1cH7H2XBvOww,1274
29
29
  wbportfolio/analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- wbportfolio/analysis/claims.py,sha256=s-eYLkIYpGveBTV-47hhe9yLzkFkI_kXAfA4ORZrOV8,10271
30
+ wbportfolio/analysis/claims.py,sha256=TKw1T1BHeJuFflUpszjsbOdO5MeOKew8yIU5f-JOCdQ,10597
31
31
  wbportfolio/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  wbportfolio/contrib/company_portfolio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  wbportfolio/contrib/company_portfolio/admin.py,sha256=roUsuOjYDWX0OGWVBjCcW3MAcwJHJYHnqZ_pLpwakmQ,549
@@ -87,11 +87,11 @@ wbportfolio/filters/esg.py,sha256=UaJEX-oT4TNVE1CgUf7nDxzyFXEMvuSqwvM4GSuTjBg,68
87
87
  wbportfolio/filters/performances.py,sha256=wIcaAfFLz31g7gBniW1c4EfRnLJ9WmiaJi7Dsdu6iOE,7324
88
88
  wbportfolio/filters/portfolios.py,sha256=aK1bI7G7ffwWGUAg5A-JmMEBu2azJjnqBoNsAUqtxcc,818
89
89
  wbportfolio/filters/positions.py,sha256=3JQE0CPdFTGTUBVm3VsBr86XgZIsU-u7Y_IfQcyIomg,7899
90
- wbportfolio/filters/products.py,sha256=Zo-k7maW4R5OrqJDB-hVTfOIyFoK1KZ8gU0nsDbfw58,6470
90
+ wbportfolio/filters/products.py,sha256=mE1cLExkUWJ3ZYGNdUuwLPZ2wBsEBEOsMjxx_oSXlx4,6491
91
91
  wbportfolio/filters/roles.py,sha256=-yDD_18nFL7aGGiIlO2R0SrFDV8eXD_9vnT_u9-6q-g,919
92
- wbportfolio/filters/signals.py,sha256=by7XgXoQYnaW-4Rr1NvQDUUPusrJ0Qj5QnrXBdOhHQM,3888
93
- wbportfolio/filters/transactions/__init__.py,sha256=gGXXvQypleQOdKrH1-donElIAWtgCltm0_IqFr9pa7c,630
94
- wbportfolio/filters/transactions/claim.py,sha256=wLQUokT5x0cUXqJF8j21zNCd2eAA9qE4e2ZGzUuHO18,16331
92
+ wbportfolio/filters/signals.py,sha256=eukEjQCPOHYeMTGhq5Fm6YLoSCq71cTim-XAF6SB9cQ,3143
93
+ wbportfolio/filters/transactions/__init__.py,sha256=WHAwqZLRTAS46lLscGJYtKYWyEknsY5MqeSD0ue1r-o,662
94
+ wbportfolio/filters/transactions/claim.py,sha256=3lnS0aCFjsnt1rCNZj3JOb6cEiA18kJZTjqbvBKaWW0,16640
95
95
  wbportfolio/filters/transactions/fees.py,sha256=9iqJuF31pf8C18OyNseSIgBZPDyWxgpBWu6S7nEJaoo,2133
96
96
  wbportfolio/filters/transactions/mixins.py,sha256=TEV3MUsiQTeu4NdFYHMIIMonmC7CdFF80JTpWYIvfRQ,550
97
97
  wbportfolio/filters/transactions/trades.py,sha256=wXWBKe-yv2ihpOlccRvQoyMQ6Oi_JSiIvGV6mtdqpfA,9341
@@ -241,14 +241,15 @@ wbportfolio/migrations/0068_trade_internal_trade_trade_marked_as_internal_and_mo
241
241
  wbportfolio/migrations/0069_remove_portfolio_is_invested_and_more.py,sha256=yIW_OLyGjX8OdgcDXUU5OXgAm9vlsYwQm5KG4tgXPqI,2106
242
242
  wbportfolio/migrations/0070_remove_assetposition_unique_asset_position_and_more.py,sha256=O4YNqVhCIA5o_KkY82eJchFmec-qGl-FOYgahkYwyHE,3222
243
243
  wbportfolio/migrations/0071_alter_trade_options_alter_trade_order.py,sha256=QjAyQr1eSs2X73zL03uG_MjfcGZhSJV9YQ0UJ39FpVk,695
244
+ wbportfolio/migrations/0072_trade_diff_shares.py,sha256=aTKa1SbIiwmlXaFtBg-ENrSxfM_cf3RPNQBQlk2VEZ0,635
244
245
  wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
245
246
  wbportfolio/models/__init__.py,sha256=ORf7HHu7PUwIN7XTev_n6USl0r484W4NiyRLtKmnIn0,992
246
247
  wbportfolio/models/adjustments.py,sha256=kd6ufcB8LkxQiQf_enXwGnLRYQ2afLTh5oxeCNyg4ZI,10143
247
- wbportfolio/models/asset.py,sha256=RLZ7YROzL7oEGObzTTWMP1tN4X9DybMlUrFeZbtuRTo,36726
248
+ wbportfolio/models/asset.py,sha256=2iYTFXkQ5BAvDJe1KfLQBZO7qg3R2YoDZWMUAg7klis,36979
248
249
  wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
249
250
  wbportfolio/models/indexes.py,sha256=GLtT8QFOkMzHSsV5ucCgL9YgkIpjQQan1YX3YmQ5cfc,1071
250
- wbportfolio/models/portfolio.py,sha256=F6fdFDIqazeIfPfn5pHRGm8Z7yqNQg8TORD27AoFfSc,47338
251
- wbportfolio/models/portfolio_cash_flow.py,sha256=ffE2PMK6tYnipat06vcZLoEo5YA-IZhNl0CbjGiBhzE,7152
251
+ wbportfolio/models/portfolio.py,sha256=DhDzyDnk4BzEKkf057RrxzNKPJpZcSDOMOEzvRxrNyo,47434
252
+ wbportfolio/models/portfolio_cash_flow.py,sha256=sh1NFCLu69Dbx71QVX0UvdBQlCl2-kBfDzhBBERnXbs,7272
252
253
  wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
253
254
  wbportfolio/models/portfolio_relationship.py,sha256=b6DF5NVtyzcuqIr7VqvTrjvQqeblC0KeF0EYUlZyWEY,5388
254
255
  wbportfolio/models/portfolio_swing_pricings.py,sha256=_LqYC1VRjnnFFmVqFPRdnbgYsVxocMVpClTk2dnooig,1778
@@ -275,7 +276,7 @@ wbportfolio/models/transactions/dividends.py,sha256=BPgIC4-c_cgISlcVza-MAUeCNI0M
275
276
  wbportfolio/models/transactions/expiry.py,sha256=vnNHdcC1hf2HP4rAbmoGgOfagBYKNFytqOwzOI0MlVI,144
276
277
  wbportfolio/models/transactions/fees.py,sha256=DLCl8HFUpti2XE7C61BNKdZx6EPY8CtQYoyCSD97q2s,5711
277
278
  wbportfolio/models/transactions/trade_proposals.py,sha256=tdWurtxxf-95-RV435OXrwOuvOXsyfmD8Jedvj6gaTA,20175
278
- wbportfolio/models/transactions/trades.py,sha256=0PEoaCu7dyqWZXuWFEympIRz8WNXzB7DMjuLfZ7si_c,26880
279
+ wbportfolio/models/transactions/trades.py,sha256=CPup2hj7RqlSFzC1ExbZsZDHiHvcADy2nOThGRNMD0k,27058
279
280
  wbportfolio/models/transactions/transactions.py,sha256=krVtVcBOYGMMM9CIrNc1u5k7h9Btvcre5arIbjuYoUU,7561
280
281
  wbportfolio/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
281
282
  wbportfolio/pms/typing.py,sha256=VReB9w5MlZ07VMKhmcSzJBzKJzbVZXUNE7NQkvwH6uk,6655
@@ -299,7 +300,7 @@ wbportfolio/risk_management/backends/stop_loss_portfolio.py,sha256=nEx5U6ao-qptu
299
300
  wbportfolio/risk_management/backends/ucits_portfolio.py,sha256=lnQNIngc9Tsni6irYIqTP_IERS1ErLlA5Te1MYsDeRw,3365
300
301
  wbportfolio/risk_management/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
301
302
  wbportfolio/risk_management/tests/conftest.py,sha256=ChVocx8SDmh13ixkRmnKe_MMudQAX0495o7nG4NDlsg,440
302
- wbportfolio/risk_management/tests/test_accounts.py,sha256=D0S4c5AT9-q8AUz_KPyLh-GKDWjF8JvI0jbWIen6Z80,3393
303
+ wbportfolio/risk_management/tests/test_accounts.py,sha256=xh2vdegoOVpb9E60c4qOXqN1Kd3qOBCLvyCw9jp9qZQ,3989
303
304
  wbportfolio/risk_management/tests/test_controversy_portfolio.py,sha256=EMOn5VLNPES0aUo-rECRcBoeltlqkt7esBZf8aMOsBQ,1365
304
305
  wbportfolio/risk_management/tests/test_exposure_portfolio.py,sha256=PUt7ZkHKSXQ79utA-wmBjP4Onp0r8Aggosk2tgvYgVk,3511
305
306
  wbportfolio/risk_management/tests/test_instrument_list_portfolio.py,sha256=b5xiFc3O5pZ2Rsnrr6qtWPxGOpwPU7g-WiAIOUq2B3o,2762
@@ -330,7 +331,7 @@ wbportfolio/serializers/transactions/claim.py,sha256=EWLWuf92akq6LARi_CSJZD4vZvf
330
331
  wbportfolio/serializers/transactions/dividends.py,sha256=EULwKDumHBv4r2HsdEGZMZGFaye4dRUNNyXg6-wZXzc,520
331
332
  wbportfolio/serializers/transactions/expiry.py,sha256=K3XOSbCyef-xRzOjCr4Qg_YFJ_JuuiJ9u6tDS86l0hg,477
332
333
  wbportfolio/serializers/transactions/fees.py,sha256=Q0MFtl9fbmiGXwxmg58UTXWRaM-L7rcNknL6NKF25tE,1067
333
- wbportfolio/serializers/transactions/trades.py,sha256=UVce7MPHGuRLBympPVacpgOfkZ2dIWUeWKhdgyM6geU,11638
334
+ wbportfolio/serializers/transactions/trades.py,sha256=B29WJks8PCdI3Jh4wQTnG5epMF5K4k629C9iRUb7SSQ,11591
334
335
  wbportfolio/serializers/transactions/transactions.py,sha256=X3ME7j5gR2oaTO84wcV4VzrGYSbZ9uPhC7tTbkPVOgQ,4184
335
336
  wbportfolio/static/wbportfolio/css/macro_review.css,sha256=FAVVO8nModxwPXcTKpcfzVxBGPZGJVK1Xn-0dkSfGyc,233
336
337
  wbportfolio/static/wbportfolio/markdown/documentation/account_holding_reconciliation.md,sha256=MabxOvOne8s5gl6osDoow6-3ghaXLAYg9THWpvy6G5I,921
@@ -495,13 +496,13 @@ wbportfolio/viewsets/configs/titles/roles.py,sha256=0HNUY3W7EFw36GMmz5zplwloU1yS
495
496
  wbportfolio/viewsets/configs/titles/trades.py,sha256=_mx27Oy2nFFEm8GyC4dnpYATpG2qnQDjaUapTxA_OPo,1985
496
497
  wbportfolio/viewsets/configs/titles/transactions.py,sha256=y33FHEnd_S37vQpL2ZMevHfYF24DOANRCqBJ7HNZqQU,327
497
498
  wbportfolio/viewsets/transactions/__init__.py,sha256=3diCf2o_2AtZbUgAnm61Ntt8-a4dNwwnDfBLe_Cm6Ag,1174
498
- wbportfolio/viewsets/transactions/claim.py,sha256=ChwsNmxu3jmEj90FYB4LJ-ciV7F0mdpbCO7Ppn37wCw,37699
499
+ wbportfolio/viewsets/transactions/claim.py,sha256=RwCR-7JPJD9l_DT5tefm32viJ4kWcFd0dwUlKhpbzBU,38772
499
500
  wbportfolio/viewsets/transactions/fees.py,sha256=GR7Cikz4Adv9R5KQSj6IUubxhjCrfDgrDKoIrnxo7N4,7030
500
501
  wbportfolio/viewsets/transactions/mixins.py,sha256=i9ICaUXZfryIrbgS-bdCcoBJO-pTnnoFKvW5zK3qRKQ,635
501
502
  wbportfolio/viewsets/transactions/trade_proposals.py,sha256=5mgAk60cINZHd2HfTu7StFOc882FGakNcIVZpNSwBCs,3528
502
- wbportfolio/viewsets/transactions/trades.py,sha256=_rZuPRDjf3MbwZ5xvdCgczCxcIbvI-RvyAf-50Y_Ga4,14744
503
- wbportfolio/viewsets/transactions/transactions.py,sha256=VgSBUs1g5etsXjt7zi4wQi82XwOGuHsd_zppnsbMZMs,4517
504
- wbportfolio-1.44.2.dist-info/METADATA,sha256=gCo3d7CxhVgXlaFxVZc4q440peMJml7yZrFhhgMNr4U,645
505
- wbportfolio-1.44.2.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
506
- wbportfolio-1.44.2.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
507
- wbportfolio-1.44.2.dist-info/RECORD,,
503
+ wbportfolio/viewsets/transactions/trades.py,sha256=GcVwfjU4N_BKXEjoKYnrtfey9tUj0-H3vdnEZfzTJy0,15457
504
+ wbportfolio/viewsets/transactions/transactions.py,sha256=HIQPGH5oDDGo1e2r3NbHXh68-yZcWhHmUBqOcwcjCRU,4449
505
+ wbportfolio-1.44.4.dist-info/METADATA,sha256=_M-1vC2g0H1H6iHLe1OFddwOnDSvim10Xtyz98W58e4,645
506
+ wbportfolio-1.44.4.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
507
+ wbportfolio-1.44.4.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
508
+ wbportfolio-1.44.4.dist-info/RECORD,,