wbfdm 1.54.20__py2.py3-none-any.whl → 1.55.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 wbfdm might be problematic. Click here for more details.

@@ -0,0 +1,49 @@
1
+ from datetime import date, timedelta
2
+ from typing import Iterator
3
+
4
+ import pypika as pk
5
+ from django.db import connections
6
+ from pypika import Case
7
+ from pypika import functions as fn
8
+ from pypika.enums import Order, SqlTypes
9
+ from wbcore.contrib.dataloader.dataloaders import Dataloader
10
+ from wbcore.contrib.dataloader.utils import dictfetchall
11
+
12
+ from wbfdm.dataloaders.protocols import FXRateProtocol
13
+ from wbfdm.dataloaders.types import FXRateDict
14
+
15
+
16
+ class DatastreamFXRatesDataloader(FXRateProtocol, Dataloader):
17
+ def fx_rates(
18
+ self,
19
+ from_date: date,
20
+ to_date: date,
21
+ target_currency: str,
22
+ ) -> Iterator[FXRateDict]:
23
+ currencies = list(self.entities.values_list("currency__key", flat=True))
24
+ # Define tables
25
+ fx_rate = pk.Table("DS2FxRate")
26
+ fx_code = pk.Table("DS2FxCode")
27
+
28
+ # Base query to get data we always need unconditionally
29
+ query = (
30
+ pk.MSSQLQuery.from_(fx_rate)
31
+ # We join on _codes, which removes all instruments not in _codes - implicit where
32
+ .join(fx_code)
33
+ .on(fx_rate.ExRateIntCode == fx_code.ExRateIntCode)
34
+ .where((fx_rate.ExRateDate >= from_date) & (fx_rate.ExRateDate <= to_date + timedelta(days=1)))
35
+ .where(
36
+ (fx_code.ToCurrCode == target_currency)
37
+ & (fx_code.FromCurrCode.isin(currencies))
38
+ & (fx_code.RateTypeCode == "SPOT")
39
+ )
40
+ .orderby(fx_rate.ExRateDate, order=Order.desc)
41
+ .select(
42
+ fn.Cast(fx_rate.ExRateDate, SqlTypes.DATE).as_("fx_date"),
43
+ fn.Concat(fx_code.FromCurrCode, fx_code.ToCurrCode).as_("currency_pair"),
44
+ (Case().when(fx_code.FromCurrCode == target_currency, 1).else_(1 / fx_rate.midrate)).as_("fx_rate"),
45
+ )
46
+ )
47
+ with connections["qa"].cursor() as cursor:
48
+ cursor.execute(query.get_sql())
49
+ yield from dictfetchall(cursor, FXRateDict)
@@ -1,3 +1,4 @@
1
+ from collections import defaultdict
1
2
  from contextlib import suppress
2
3
  from datetime import date
3
4
  from enum import Enum
@@ -7,13 +8,14 @@ from typing import TYPE_CHECKING, Iterator
7
8
 
8
9
  import pypika as pk
9
10
  from django.db import ProgrammingError, connections
10
- from pypika import Case, Column, MSSQLQuery
11
+ from pypika import Column, MSSQLQuery
11
12
  from pypika import functions as fn
12
13
  from pypika.enums import Order, SqlTypes
13
14
  from pypika.terms import ValueWrapper
14
15
  from wbcore.contrib.dataloader.dataloaders import Dataloader
15
16
  from wbcore.contrib.dataloader.utils import dictfetchall
16
17
 
18
+ from wbfdm.contrib.qa.dataloaders.fx_rates import DatastreamFXRatesDataloader
17
19
  from wbfdm.contrib.qa.dataloaders.utils import create_table
18
20
  from wbfdm.dataloaders.protocols import MarketDataProtocol
19
21
  from wbfdm.dataloaders.types import MarketDataDict
@@ -60,10 +62,19 @@ class DatastreamMarketDataDataloader(MarketDataProtocol, Dataloader):
60
62
  Returns:
61
63
  Iterator[MarketDataDict]: An iterator of dictionaries conforming to the DailyValuationDict.
62
64
  """
63
-
64
65
  lookup = {
65
66
  f"{k[0]},{k[1]}": v for k, v in self.entities.values_list("dl_parameters__market_data__parameters", "id")
66
67
  }
68
+ fx_rates = defaultdict(dict)
69
+ if target_currency:
70
+ if exact_date:
71
+ from_date = exact_date
72
+ to_date = exact_date
73
+ if from_date and to_date:
74
+ for fx_rate in DatastreamFXRatesDataloader(self.entities).fx_rates(
75
+ from_date, to_date, target_currency
76
+ ):
77
+ fx_rates[fx_rate["currency_pair"]][fx_rate["fx_date"]] = fx_rate["fx_rate"]
67
78
 
68
79
  # Define tables
69
80
  pricing = pk.Table("vw_DS2Pricing")
@@ -90,33 +101,7 @@ class DatastreamMarketDataDataloader(MarketDataProtocol, Dataloader):
90
101
  .orderby(pricing.MarketDate, order=Order.desc)
91
102
  )
92
103
 
93
- # if a target currency is required, we join on the fx tables and set the currency to the desired one
94
- # otherwise we just set the currency to whatever the currency is from the instrument
95
- fx_rate = None
96
- if target_currency:
97
- query = query.select(ValueWrapper(target_currency).as_("currency"))
98
- fx_code = pk.Table("DS2FxCode")
99
- fx_rate = pk.Table("DS2FxRate")
100
- query = (
101
- query.select(
102
- (Case().when(pricing.Currency == target_currency, 1).else_(1 / fx_rate.midrate)).as_("fx_rate")
103
- )
104
- # Join FX code table matching currencies and ensuring SPOT rate type
105
- .left_join(fx_code)
106
- .on(
107
- (fx_code.FromCurrCode == pricing.Currency)
108
- & (fx_code.ToCurrCode == target_currency)
109
- & (fx_code.RateTypeCode == "SPOT")
110
- )
111
- # Join FX rate table matching internal code and date
112
- .left_join(fx_rate)
113
- .on((fx_rate.ExRateIntCode == fx_code.ExRateIntCode) & (fx_rate.ExRateDate == pricing.MarketDate))
114
- # We filter out rows which do not have a proper fx rate (we exclude same currency conversions)
115
- .where((Case().when(pricing.Currency == target_currency, 1).else_(fx_rate.midrate).isnotnull()))
116
- )
117
-
118
- else:
119
- query = query.select(pricing.Currency.as_("currency"))
104
+ query = query.select(pricing.Currency.as_("currency"))
120
105
 
121
106
  # if market cap or shares outstanding are required we need to join with an additional table
122
107
  if MarketData.MARKET_CAPITALIZATION in values or MarketData.SHARES_OUTSTANDING in values:
@@ -130,9 +115,6 @@ class DatastreamMarketDataDataloader(MarketDataProtocol, Dataloader):
130
115
  )
131
116
 
132
117
  value = pricing_2.Close_
133
- if fx_rate and apply_fx_rate:
134
- value /= Case().when(pricing_2.Currency == target_currency, 1).else_(fx_rate.midrate)
135
-
136
118
  query = query.select(value.as_("undadjusted_close"))
137
119
  query = query.select(
138
120
  MSSQLQuery.from_(num_shares)
@@ -148,9 +130,6 @@ class DatastreamMarketDataDataloader(MarketDataProtocol, Dataloader):
148
130
  ):
149
131
  ds2_value = DS2MarketData[market_data.name].value
150
132
  value = getattr(pricing, ds2_value)
151
- if fx_rate and apply_fx_rate and market_data is not MarketData.SHARES_OUTSTANDING:
152
- value /= Case().when(pricing.Currency == target_currency, 1).else_(fx_rate.midrate)
153
-
154
133
  query = query.select(value.as_(market_data.value))
155
134
 
156
135
  # Add conditional where clauses
@@ -189,7 +168,21 @@ class DatastreamMarketDataDataloader(MarketDataProtocol, Dataloader):
189
168
 
190
169
  if MarketData.SHARES_OUTSTANDING in values:
191
170
  row["outstanding_shares"] = (row["market_capitalization"] / row["close"]) if row["close"] else None
192
-
171
+ if target_currency:
172
+ try:
173
+ if target_currency == row["currency"]:
174
+ fx_rate = 1.0
175
+ else:
176
+ fx_rate = fx_rates[f'{row["currency"]}{target_currency}'][row["valuation_date"]] or 1.0
177
+ if apply_fx_rate:
178
+ for e in MarketData:
179
+ if e != MarketData.SHARES_OUTSTANDING and e.value in row and row[e.value]:
180
+ row[e.value] = row[e.value] * fx_rate
181
+ row["currency"] = target_currency
182
+ row["fx_rate"] = fx_rate
183
+ except KeyError:
184
+ # if we don't find the fx rate but we asked for it, we invalid that row and do not return it
185
+ continue
193
186
  yield row
194
187
 
195
188
  cursor.execute(MSSQLQuery.drop_table(mapping).get_sql())
@@ -239,6 +239,10 @@ class RKDStatementsDataloader(StatementsProtocol, Dataloader):
239
239
  ) -> Iterator[StatementDataDict]:
240
240
  lookup = {k: v for k, v in self.entities.values_list("dl_parameters__statements__parameters", "id")}
241
241
  sql = reported_sql if data_type is DataType.REPORTED else standardized_sql
242
+ if not financials:
243
+ financials = []
244
+ external_codes = [RKDFinancial[fin.name].value for fin in financials if fin.name in RKDFinancial.__members__]
245
+
242
246
  query, bind_params = JinjaSql(param_style="format").prepare_query(
243
247
  sql,
244
248
  {
@@ -249,7 +253,7 @@ class RKDStatementsDataloader(StatementsProtocol, Dataloader):
249
253
  "from_date": from_date,
250
254
  "to_date": to_date,
251
255
  "period_type": period_type.value,
252
- "external_codes": [RKDFinancial[fin.name].value for fin in financials or []],
256
+ "external_codes": external_codes,
253
257
  },
254
258
  )
255
259
  with connections["qa"].cursor() as cursor:
@@ -264,5 +268,8 @@ class RKDStatementsDataloader(StatementsProtocol, Dataloader):
264
268
  row["year"] = int(row["year"] or row["period_end_date"].year)
265
269
  row["instrument_id"] = lookup[row["external_identifier"]]
266
270
  if financials:
267
- row["financial"] = Financial[RKDFinancial(row["external_code"]).name].value
271
+ try:
272
+ row["financial"] = Financial[RKDFinancial(row["external_code"]).name].value
273
+ except (ValueError, KeyError):
274
+ continue
268
275
  yield row
@@ -7,6 +7,7 @@ from wbfdm.dataloaders.types import (
7
7
  ESGControversyDataDict,
8
8
  ESGDataDict,
9
9
  FinancialDataDict,
10
+ FXRateDict,
10
11
  MarketDataDict,
11
12
  OfficerDataDict,
12
13
  ReportDateDataDict,
@@ -36,6 +37,15 @@ class AdjustmentsProtocol(Protocol):
36
37
  ) -> Iterator[AdjustmentDataDict]: ...
37
38
 
38
39
 
40
+ class FXRateProtocol(Protocol):
41
+ def fx_rates(
42
+ self,
43
+ from_date: date,
44
+ to_date: date,
45
+ target_currency: str,
46
+ ) -> Iterator[FXRateDict]: ...
47
+
48
+
39
49
  class MarketDataProtocol(Protocol):
40
50
  def market_data(
41
51
  self,
@@ -9,6 +9,7 @@ from wbfdm.dataloaders.protocols import (
9
9
  ESGControversyProtocol,
10
10
  ESGProtocol,
11
11
  FinancialsProtocol,
12
+ FXRateProtocol,
12
13
  MarketDataProtocol,
13
14
  OfficersProtocol,
14
15
  ReportDateProtocol,
@@ -20,6 +21,7 @@ from wbfdm.dataloaders.types import (
20
21
  ESGControversyDataDict,
21
22
  ESGDataDict,
22
23
  FinancialDataDict,
24
+ FXRateDict,
23
25
  MarketDataDict,
24
26
  OfficerDataDict,
25
27
  ReportDateDataDict,
@@ -53,7 +55,8 @@ def _market_data_row_parser(row):
53
55
 
54
56
  class InstrumentDataloaderProxy(
55
57
  DataloaderProxy[
56
- AdjustmentsProtocol
58
+ FXRateProtocol
59
+ | AdjustmentsProtocol
57
60
  | MarketDataProtocol
58
61
  | CorporateActionsProtocol
59
62
  | OfficersProtocol
@@ -68,6 +71,15 @@ class InstrumentDataloaderProxy(
68
71
  for dl in self.iterate_dataloaders("reporting_dates"):
69
72
  yield from dl.reporting_dates(only_next=only_next)
70
73
 
74
+ def fx_rates(
75
+ self,
76
+ from_date: date,
77
+ to_date: date,
78
+ target_currency: str,
79
+ ) -> Iterator[FXRateDict]:
80
+ for dl in self.iterate_dataloaders("fx_rates"):
81
+ yield from dl.fx_rates(from_date, to_date, target_currency)
82
+
71
83
  def adjustments(self, from_date: date | None = None, to_date: date | None = None) -> Iterator[AdjustmentDataDict]:
72
84
  for dl in self.iterate_dataloaders("adjustments"):
73
85
  yield from dl.adjustments(from_date=from_date, to_date=to_date)
@@ -26,6 +26,12 @@ class BaseDict(TypedDict):
26
26
  currency: NotRequired[str]
27
27
 
28
28
 
29
+ class FXRateDict(TypedDict):
30
+ currency_pair: str
31
+ fx_date: date
32
+ fx_rate: float
33
+
34
+
29
35
  class MarketDataDict(BaseDict):
30
36
  """
31
37
  Represents a dictionary for daily valuation data.
@@ -21,7 +21,7 @@ class InstrumentFactory(factory.django.DjangoModelFactory):
21
21
  instrument_type = factory.SubFactory(InstrumentTypeFactory)
22
22
  inception_date = factory.Faker("past_date")
23
23
  delisted_date = None
24
- currency = factory.SubFactory("wbcore.contrib.currency.factories.CurrencyFactory")
24
+ currency = factory.SubFactory("wbcore.contrib.currency.factories.CurrencyUSDFactory")
25
25
  country = factory.SubFactory("wbcore.contrib.geography.factories.CountryFactory")
26
26
  exchange = factory.SubFactory("wbfdm.factories.exchanges.ExchangeFactory")
27
27
 
@@ -54,7 +54,7 @@ class InstrumentFactory(factory.django.DjangoModelFactory):
54
54
 
55
55
  class CashFactory(factory.django.DjangoModelFactory):
56
56
  is_cash = True
57
- currency = factory.SubFactory("wbcore.contrib.currency.factories.CurrencyFactory")
57
+ currency = factory.SubFactory("wbcore.contrib.currency.factories.CurrencyUSDFactory")
58
58
  name = factory.LazyAttribute(lambda o: o.currency.title)
59
59
  instrument_type = factory.LazyAttribute(
60
60
  lambda o: InstrumentType.objects.get_or_create(key="cash", defaults={"name": "Cash", "short_name": "Cash"})[0]
@@ -26,7 +26,7 @@ class OptionAggregateImportHandler(ImportExportHandler):
26
26
 
27
27
  def _get_instance(self, data: Dict[str, Any], history: Optional[models.QuerySet] = None, **kwargs) -> models.Model:
28
28
  with suppress(ObjectDoesNotExist):
29
- return self.model.objects.filter(instrument=data["instrument"], date=data["date"], type=data["type"])
29
+ return self.model.objects.get(instrument=data["instrument"], date=data["date"], type=data["type"])
30
30
 
31
31
 
32
32
  class OptionImportHandler(ImportExportHandler):
@@ -46,7 +46,7 @@ class OptionImportHandler(ImportExportHandler):
46
46
 
47
47
  def _get_instance(self, data: Dict[str, Any], history: Optional[models.QuerySet] = None, **kwargs) -> models.Model:
48
48
  with suppress(ObjectDoesNotExist):
49
- return self.model.objects.filter(
49
+ return self.model.objects.get(
50
50
  instrument=data["instrument"],
51
51
  contract_identifier=data["contract_identifier"],
52
52
  date=data["date"],
@@ -0,0 +1,18 @@
1
+ # Generated by Django 5.0.14 on 2025-08-27 07:59
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('wbfdm', '0031_exchange_apply_round_lot_size_and_more'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='instrumentprice',
15
+ name='outstanding_shares',
16
+ field=models.DecimalField(blank=True, decimal_places=4, help_text='The amount of outstanding share for this instrument', max_digits=16, null=True, verbose_name='Outstanding Shares'),
17
+ ),
18
+ ]
@@ -195,9 +195,11 @@ class InstrumentPrice(
195
195
  verbose_name="Value (Gross)",
196
196
  ) # TODO: I think we need to remove this field that is not really used here.
197
197
 
198
- outstanding_shares = DynamicDecimalField(
198
+ outstanding_shares = models.DecimalField(
199
199
  decimal_places=4,
200
200
  max_digits=16,
201
+ blank=True,
202
+ null=True,
201
203
  verbose_name="Outstanding Shares",
202
204
  help_text="The amount of outstanding share for this instrument",
203
205
  )
@@ -139,6 +139,12 @@ class InstrumentManager(TreeManager):
139
139
  def filter_active_at_date(self, val_date: date):
140
140
  return self.get_queryset().filter_active_at_date(val_date)
141
141
 
142
+ def get_instrument_prices_from_market_data(self, **kwargs):
143
+ return self.get_queryset().get_instrument_prices_from_market_data(**kwargs)
144
+
145
+ def get_returns_df(self, **kwargs) -> tuple[dict[date, dict[int, float]], pd.DataFrame]:
146
+ return self.get_queryset().get_returns_df(**kwargs)
147
+
142
148
 
143
149
  class SecurityInstrumentManager(InstrumentManager):
144
150
  def get_queryset(self) -> InstrumentQuerySet:
@@ -697,10 +697,6 @@ import pandas as pd
697
697
  #
698
698
  #
699
699
  class InstrumentPriceComputedMixin:
700
- def _compute_outstanding_shares(self):
701
- if self.outstanding_shares is None and (previous_price := self.previous_price):
702
- return previous_price.outstanding_shares
703
-
704
700
  def _compute_outstanding_shares_consolidated(self):
705
701
  if self.outstanding_shares_consolidated is None and self.outstanding_shares is not None:
706
702
  return self.outstanding_shares
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from contextlib import suppress
3
- from datetime import date
3
+ from datetime import date, timedelta
4
4
  from decimal import Decimal
5
5
 
6
6
  import numpy as np
@@ -9,6 +9,7 @@ from django.core.exceptions import MultipleObjectsReturned, ValidationError
9
9
  from django.core.validators import DecimalValidator
10
10
  from django.db.models import (
11
11
  AutoField,
12
+ Case,
12
13
  Exists,
13
14
  ExpressionWrapper,
14
15
  F,
@@ -16,9 +17,12 @@ from django.db.models import (
16
17
  Q,
17
18
  QuerySet,
18
19
  Subquery,
20
+ Value,
21
+ When,
19
22
  )
20
23
  from django.db.models.functions import Coalesce
21
- from wbcore.contrib.currency.models import CurrencyFXRates
24
+ from skfolio.preprocessing import prices_to_returns
25
+ from wbcore.contrib.currency.models import Currency, CurrencyFXRates
22
26
 
23
27
  from wbfdm.enums import MarketData
24
28
 
@@ -177,3 +181,79 @@ class InstrumentQuerySet(QuerySet):
177
181
  yield from filter(
178
182
  lambda x: x, map(lambda row: _dict_to_object(instrument, row), dff.to_dict("records"))
179
183
  )
184
+
185
+ def get_returns_df(
186
+ self, from_date: date, to_date: date, to_currency: Currency | None = None, use_dl: bool = False
187
+ ) -> tuple[dict[date, dict[int, float]], pd.DataFrame]:
188
+ """
189
+ Utility methods to get instrument returns for a given date range
190
+
191
+ Args:
192
+ from_date: date range lower bound
193
+ to_date: date range upper bound
194
+ to_currency: currency to use for returns
195
+ use_dl: whether to get data straight from the dataloader or use the internal table
196
+
197
+ Returns:
198
+ Return a tuple of the raw prices and the returns dataframe
199
+ """
200
+ padded_from_date = from_date - timedelta(days=15)
201
+ padded_to_date = to_date + timedelta(days=3)
202
+ logger.info(
203
+ f"Loading returns from {from_date:%Y-%m-%d} (padded to {padded_from_date:%Y-%m-%d}) to {to_date:%Y-%m-%d} (padded to {padded_to_date:%Y-%m-%d}) for {self.count()} instruments"
204
+ )
205
+
206
+ if use_dl:
207
+ kwargs = dict(
208
+ from_date=padded_from_date, to_date=padded_to_date, values=[MarketData.CLOSE], apply_fx_rate=False
209
+ )
210
+ if to_currency:
211
+ kwargs["target_currency"] = to_currency.key
212
+ df = pd.DataFrame(self.dl.market_data(**kwargs))
213
+ if df.empty:
214
+ df = pd.DataFrame(columns=["instrument_id", "fx_rate", "close", "valuation_date"])
215
+ else:
216
+ df = df[["instrument_id", "fx_rate", "close", "valuation_date"]]
217
+ else:
218
+ from wbfdm.models import InstrumentPrice
219
+
220
+ if to_currency:
221
+ fx_rate = Coalesce(
222
+ CurrencyFXRates.get_fx_rates_subquery_for_two_currencies(
223
+ "date", "instrument__currency", to_currency
224
+ ),
225
+ Decimal("1"),
226
+ )
227
+ else:
228
+ fx_rate = Value(Decimal("1"))
229
+ # annotate fx rate only if the price is not calculated, in that case we assume the instrument is not tradable and we set a forex of None (to be fast forward filled)
230
+ prices = InstrumentPrice.objects.filter(
231
+ instrument__in=self, date__gte=padded_from_date, date__lte=padded_to_date
232
+ ).annotate(fx_rate=Case(When(calculated=False, then=fx_rate), default=None))
233
+ df = pd.DataFrame(
234
+ prices.filter_only_valid_prices().values_list("instrument", "fx_rate", "net_value", "date"),
235
+ columns=["instrument_id", "fx_rate", "close", "valuation_date"],
236
+ )
237
+ df = (
238
+ df.pivot_table(index="valuation_date", columns="instrument_id", values=["fx_rate", "close"], dropna=False)
239
+ .astype(float)
240
+ .sort_index()
241
+ )
242
+ if not df.empty:
243
+ ts = pd.bdate_range(df.index.min(), df.index.max(), freq="B")
244
+ df = df.reindex(ts)
245
+ df = df.ffill()
246
+ df.index = pd.to_datetime(df.index)
247
+ df = df[
248
+ (df.index <= pd.Timestamp(to_date)) & (df.index >= pd.Timestamp(from_date))
249
+ ] # ensure the returned df corresponds to requested date range
250
+ prices_df = df["close"]
251
+ if "fx_rate" in df.columns:
252
+ fx_rate_df = df["fx_rate"].fillna(1.0)
253
+ else:
254
+ fx_rate_df = pd.DataFrame(np.ones(prices_df.shape), index=prices_df.index, columns=prices_df.columns)
255
+ returns = prices_to_returns(fx_rate_df * prices_df, drop_inceptions_nan=False, fill_nan=True)
256
+ return {
257
+ ts.date(): row for ts, row in prices_df.replace([np.nan], None).to_dict("index").items()
258
+ }, returns.replace([np.inf, -np.inf, np.nan], 0)
259
+ return {}, pd.DataFrame()
wbfdm/tasks.py CHANGED
@@ -1,8 +1,9 @@
1
+ from contextlib import suppress
1
2
  from datetime import date, timedelta
2
3
 
3
4
  from celery import shared_task
4
5
  from django.db import transaction
5
- from django.db.models import Q
6
+ from django.db.models import ProtectedError, Q
6
7
  from pandas.tseries.offsets import BDay
7
8
  from tqdm import tqdm
8
9
 
@@ -89,7 +90,8 @@ def full_synchronization_as_task():
89
90
  (Q(name="") & Q(name_repr="")) | (Q(source__in=["qa-ds2-security", "qa-ds2-quote"]) & Q(parent__isnull=True))
90
91
  )
91
92
  for instrument in qs:
92
- instrument.delete()
93
+ with suppress(ProtectedError):
94
+ instrument.delete()
93
95
  initialize_exchanges()
94
96
  initialize_instruments()
95
97
  with transaction.atomic():
@@ -1,10 +1,8 @@
1
1
  from datetime import date
2
- from decimal import Decimal
3
2
 
4
3
  import pandas as pd
5
4
  import pytest
6
5
  from faker import Faker
7
- from pandas.tseries.offsets import BDay
8
6
  from wbcore.models import DynamicDecimalField, DynamicFloatField
9
7
 
10
8
  from wbfdm.models import Instrument, InstrumentPrice, RelatedInstrumentThroughModel
@@ -168,18 +166,6 @@ class TestInstrumentPriceModel:
168
166
  assert isinstance(instrument_price._meta.get_field("gross_value"), DynamicDecimalField)
169
167
  assert instrument_price.gross_value == instrument_price.net_value
170
168
 
171
- @pytest.mark.parametrize("instrument_price__outstanding_shares", [Decimal(10)])
172
- def test_compute_outstanding_shares(self, instrument_price, instrument_price_factory):
173
- next_price = instrument_price_factory.create(
174
- instrument=instrument_price.instrument,
175
- date=instrument_price.date + BDay(1),
176
- outstanding_shares=None,
177
- calculated=instrument_price.calculated,
178
- )
179
- assert hasattr(instrument_price, "_compute_outstanding_shares")
180
- assert isinstance(instrument_price._meta.get_field("outstanding_shares"), DynamicDecimalField)
181
- assert next_price.outstanding_shares == instrument_price.outstanding_shares
182
-
183
169
  @pytest.mark.parametrize("instrument_price__volume_50d", [None])
184
170
  def test_compute_volume_50d(self, instrument_price, instrument_price_factory):
185
171
  assert hasattr(instrument_price, "_compute_volume_50d")
@@ -0,0 +1,89 @@
1
+ from datetime import date
2
+ from decimal import Decimal
3
+
4
+ import pandas as pd
5
+ import pytest
6
+
7
+ from wbfdm.models import Instrument
8
+
9
+
10
+ @pytest.mark.django_db
11
+ class TestInstrumentQueryset:
12
+ def test_get_returns(self, instrument_factory, instrument_price_factory):
13
+ v1 = date(2024, 12, 31)
14
+ v2 = date(2025, 1, 1)
15
+ v3 = date(2025, 1, 2)
16
+ v4 = date(2025, 1, 3)
17
+
18
+ i1 = instrument_factory.create()
19
+ i2 = instrument_factory.create()
20
+
21
+ i11 = instrument_price_factory.create(date=v1, instrument=i1)
22
+ i12 = instrument_price_factory.create(date=v2, instrument=i1)
23
+ i14 = instrument_price_factory.create(date=v4, instrument=i1)
24
+ i21 = instrument_price_factory.create(date=v1, instrument=i2)
25
+ i11.refresh_from_db()
26
+ i12.refresh_from_db()
27
+ i14.refresh_from_db()
28
+ prices, returns = Instrument.objects.filter(id__in=[i1.id, i2.id]).get_returns_df(from_date=v1, to_date=v4)
29
+
30
+ expected_returns = pd.DataFrame(
31
+ [[i12.net_value / i11.net_value - 1, 0.0], [0.0, 0.0], [i14.net_value / i12.net_value - 1, 0.0]],
32
+ index=[v2, v3, v4],
33
+ columns=[i1.id, i2.id],
34
+ dtype="float64",
35
+ )
36
+ expected_returns.index = pd.to_datetime(expected_returns.index)
37
+ pd.testing.assert_frame_equal(returns, expected_returns, check_names=False, check_freq=False, atol=1e-6)
38
+ assert prices[v1][i1.id] == float(i11.net_value)
39
+ assert prices[v2][i1.id] == float(i12.net_value)
40
+ assert prices[v3][i1.id] == float(i12.net_value)
41
+ assert prices[v4][i1.id] == float(i14.net_value)
42
+ # test that the returned price are ffill
43
+ assert prices[v1][i2.id] == float(i21.net_value)
44
+ assert prices[v2][i2.id] == float(i21.net_value)
45
+ assert prices[v3][i2.id] == float(i21.net_value)
46
+ assert prices[v4][i2.id] == float(i21.net_value)
47
+
48
+ def test_get_returns_fix_forex_on_holiday(
49
+ self, instrument, instrument_price_factory, currency_fx_rates_factory, currency_factory
50
+ ):
51
+ v1 = date(2024, 12, 31)
52
+ v2 = date(2025, 1, 1)
53
+ v3 = date(2025, 1, 2)
54
+
55
+ target_currency = currency_factory.create()
56
+ fx_target1 = currency_fx_rates_factory.create(currency=target_currency, date=v1)
57
+ fx_target2 = currency_fx_rates_factory.create(currency=target_currency, date=v2) # noqa
58
+ fx_target3 = currency_fx_rates_factory.create(currency=target_currency, date=v3)
59
+
60
+ fx1 = currency_fx_rates_factory.create(currency=instrument.currency, date=v1)
61
+ fx2 = currency_fx_rates_factory.create(currency=instrument.currency, date=v2) # noqa
62
+ fx3 = currency_fx_rates_factory.create(currency=instrument.currency, date=v3)
63
+
64
+ i1 = instrument_price_factory.create(net_value=Decimal("100"), date=v1, instrument=instrument)
65
+ i2 = instrument_price_factory.create(net_value=Decimal("100"), date=v2, instrument=instrument, calculated=True)
66
+ i3 = instrument_price_factory.create(net_value=Decimal("200"), date=v3, instrument=instrument)
67
+
68
+ prices, returns = Instrument.objects.filter(id__in=[instrument.id]).get_returns_df(
69
+ from_date=v1, to_date=v3, to_currency=target_currency
70
+ )
71
+ returns.index = pd.to_datetime(returns.index)
72
+ assert prices[v1][instrument.id] == float(i1.net_value)
73
+ assert prices[v2][instrument.id] == float(i2.net_value)
74
+ assert prices[v3][instrument.id] == float(i3.net_value)
75
+
76
+ assert returns.loc[pd.Timestamp(v2), instrument.id] == pytest.approx(
77
+ float(
78
+ (i2.net_value * fx_target1.value / fx1.value) / (i1.net_value * fx_target1.value / fx1.value)
79
+ - Decimal("1")
80
+ ),
81
+ abs=10e-8,
82
+ ) # as v2 as a calculated price, the forex won't apply to it
83
+ assert returns.loc[pd.Timestamp(v3), instrument.id] == pytest.approx(
84
+ float(
85
+ (i3.net_value * fx_target3.value / fx3.value) / (i2.net_value * fx_target1.value / fx1.value)
86
+ - Decimal("1")
87
+ ),
88
+ abs=10e-8,
89
+ )
@@ -174,22 +174,22 @@ class InstrumentPriceInstrumentStatisticsChartView(InstrumentMixin, viewsets.Cha
174
174
 
175
175
  if self.instrument.related_instruments.count() > 0:
176
176
  reference = self.instrument.related_instruments.first().name_repr
177
- risk = self.instrument.primary_risk_instrument.name_repr
178
177
  df = pd.DataFrame(queryset.values("date", "sharpe_ratio", "correlation", "beta")).replace(
179
178
  [np.inf, -np.inf], np.nan
180
179
  )
181
180
 
182
181
  if not df.empty:
183
182
  df = df.set_index("date").sort_index().dropna()
184
- fig.add_trace(
185
- go.Scatter(
186
- x=df.index,
187
- y=df.sharpe_ratio,
188
- mode="lines",
189
- name=f"Sharpe Ratio ({risk})",
190
- hovertemplate=get_hovertemplate_timeserie(currency=""),
183
+ if risk_instrument := self.instrument.primary_risk_instrument:
184
+ fig.add_trace(
185
+ go.Scatter(
186
+ x=df.index,
187
+ y=df.sharpe_ratio,
188
+ mode="lines",
189
+ name=f"Sharpe Ratio ({risk_instrument.name_repr})",
190
+ hovertemplate=get_hovertemplate_timeserie(currency=""),
191
+ )
191
192
  )
192
- )
193
193
  fig.add_trace(
194
194
  go.Scatter(
195
195
  x=df.index,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbfdm
3
- Version: 1.54.20
3
+ Version: 1.55.4
4
4
  Summary: The workbench module ensures rapid access to diverse financial data (market, fundamental, forecasts, ESG), with features for storing instruments, classifying them, and conducting financial analysis.
5
5
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
6
6
  Requires-Dist: roman==4.*
@@ -5,7 +5,7 @@ wbfdm/enums.py,sha256=5AuUouk5uuSNmRc6e-SiBu4FPmHVTN60ol9ftiuVrAc,33041
5
5
  wbfdm/jinja2.py,sha256=pkIC1U-0rf6vn0DDEUzZ8dPYiTGEPY8LBTRMi9wYiuc,199
6
6
  wbfdm/preferences.py,sha256=8ghDcaapOMso1kjtNfKbSFykPUTxzqI5R77gM3BgiMs,927
7
7
  wbfdm/signals.py,sha256=PhAsFpQZF1YVe5UpedaRelUD_TVjemqRYm1HzV-bhmY,597
8
- wbfdm/tasks.py,sha256=6rI2SH94ZGL4ycjNX8cntoYzUcb_wJciNmAomGWBILs,4711
8
+ wbfdm/tasks.py,sha256=zGlzMl-kTWQjkFRRVbaHdcwIaPTgxOJ-ZpyvXmqidFk,4802
9
9
  wbfdm/urls.py,sha256=pDp9I0kktxicad8sXXEUT7402jZPMJNcE5R1doTlcMw,8887
10
10
  wbfdm/utils.py,sha256=4cWrCpqXxHIjtSlt4DDPFvmtaqXw_H0nqhM6sGuXx0o,1938
11
11
  wbfdm/admin/__init__.py,sha256=Z1VtH_gjD71K79KcD-2Q2Lu_p_7j0akMZj7gNxdz1CQ,1398
@@ -100,10 +100,11 @@ wbfdm/contrib/qa/dataloaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
100
100
  wbfdm/contrib/qa/dataloaders/adjustments.py,sha256=DQEexOLA7WyBB1dZJHQd-6zbzyEIURgSSgS7bJRvXzQ,2980
101
101
  wbfdm/contrib/qa/dataloaders/corporate_actions.py,sha256=lWT6klrTXKqxiko2HGrxHH8E2C00FJS-AOX3IhglRrI,2912
102
102
  wbfdm/contrib/qa/dataloaders/financials.py,sha256=xUHpvhUkvmdPL_RyWCrs7XgChgTklX5qemmaXMedgkY,3475
103
- wbfdm/contrib/qa/dataloaders/market_data.py,sha256=ei_XNzgMOU-X8dCR06xQGASboCmw05-Hwgn1t17n5ME,8318
103
+ wbfdm/contrib/qa/dataloaders/fx_rates.py,sha256=IYkUV8_8Vmvm4_K9xJpz7VaTgjQUz0y4pb3KyaoiqCM,1985
104
+ wbfdm/contrib/qa/dataloaders/market_data.py,sha256=-HvEcJr-D37uWomnCegrhjDAsyxJsEKUayUAIlqjm24,8002
104
105
  wbfdm/contrib/qa/dataloaders/officers.py,sha256=vytlQJJxmn4Y5HfNh5mHJAvuIrrsQSkNO-sONyhxftY,2940
105
106
  wbfdm/contrib/qa/dataloaders/reporting_dates.py,sha256=q25ccB0pbGfLJLV1A1_AY1XYWJ_Fa10egY09L1J-C5A,2628
106
- wbfdm/contrib/qa/dataloaders/statements.py,sha256=hC6YErJcvBTmaAmzscgeC4sBK3lYE2U5eIKRIE9b_cs,10094
107
+ wbfdm/contrib/qa/dataloaders/statements.py,sha256=6k8dDwJPLY6XE3G5ZA03_4wRvT7XduRsro4lzuAWCvM,10337
107
108
  wbfdm/contrib/qa/dataloaders/utils.py,sha256=E0qav459E7razVOvHKVt9ld_gteJ6eQ2oR4xN-CIOns,2941
108
109
  wbfdm/contrib/qa/jinja2/qa/sql/companies.sql,sha256=RzTkfVjBVOgyirgKxp2rnJdjsKl8d3YM-d2qdjHx9cw,3801
109
110
  wbfdm/contrib/qa/jinja2/qa/sql/instruments.sql,sha256=7p31aJDtKusfSzhrjyUo8FlmbPXNyffWtNclm8DZkFM,3863
@@ -118,16 +119,16 @@ wbfdm/contrib/qa/sync/instruments.py,sha256=8aTQVJ_cw1phe4FWikn79pjCfUijaTcwkdhQ
118
119
  wbfdm/contrib/qa/sync/utils.py,sha256=LUnjNR28moT92cjP04SVCRQ_Ssp6SaO9kehgWV4zvOs,11782
119
120
  wbfdm/dataloaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
120
121
  wbfdm/dataloaders/cache.py,sha256=K9BeVxT7p-BMvjurINt18bfrUDccp840uIjfDBLJRNk,4841
121
- wbfdm/dataloaders/protocols.py,sha256=1vornH4tCIsd7HLL3lCiE0ni6YQEI6fF6Em_I-AUSFc,3023
122
- wbfdm/dataloaders/proxies.py,sha256=gA5QFXgbC78tQpmypow1dJ5k7UyqFGXDEyeS-xpUGEw,7157
123
- wbfdm/dataloaders/types.py,sha256=8PNHfBQj4C0xVn6cJxKQV3oBFxkTW3MGn4dMMRKVGJ4,5596
122
+ wbfdm/dataloaders/protocols.py,sha256=LPuf3edDpP_jkqJpuiakbDR-XNgOxxN5JdDAo2f93Ms,3218
123
+ wbfdm/dataloaders/proxies.py,sha256=1BTY7w3A32axWEOhP9fPZtZHGxHY2GzguteWRSxdhnM,7488
124
+ wbfdm/dataloaders/types.py,sha256=VUqZgZJZ0mBnFp1T7AaQuWfplMx4AkPA_u52qqo8PQ4,5687
124
125
  wbfdm/factories/__init__.py,sha256=yYxAKBde_ksIr-3g4RjL6d5Wu-nmsuEDdYNyJpgfpQU,660
125
126
  wbfdm/factories/classifications.py,sha256=GJ0eARFTsj5dnKUsUYbLSIZLzTCy7RWhy7_f8Dn6IMQ,1827
126
127
  wbfdm/factories/controversies.py,sha256=GhuoEms1O7DIMVNAIbFEzD9DRv8j1MXIv0u1JK6Pi-o,929
127
128
  wbfdm/factories/exchanges.py,sha256=heJHQE59dCDFeDuScJJti4C_SsMsz3O0kmczpGYVNlQ,831
128
129
  wbfdm/factories/instrument_list.py,sha256=ypnrTLCl0XRfGj6y-3XJSQ2Wl5TULxZU0I3nF6svH3Y,743
129
130
  wbfdm/factories/instrument_prices.py,sha256=EjRFbMjP3pLrxoNsNqNo37FGjayIiV99bkQPVgZLj4I,3623
130
- wbfdm/factories/instruments.py,sha256=A4BP06Ma66uG6klsfkMjGfiP9WYh85OZPTRiXFxwUM0,2552
131
+ wbfdm/factories/instruments.py,sha256=sJqHNA6Cs-NNm9RQrCimUVjKTrJS-ajjpTP4taPRTqA,2558
131
132
  wbfdm/factories/instruments_relationships.py,sha256=opGQMM3sHQV5F04nGPCCsRw8ux8vSQ78tHNJjIDPyUE,1138
132
133
  wbfdm/factories/options.py,sha256=nna8LgB_2-XNGm37--Edkdv1oc49oeKtr7f8tcIJPU4,2463
133
134
  wbfdm/figures/__init__.py,sha256=PDF_OWeTocmJIQozLxj_OjDUeUG7OYzcS2DLpe-ECEA,49
@@ -166,7 +167,7 @@ wbfdm/import_export/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
166
167
  wbfdm/import_export/handlers/instrument.py,sha256=ZtXwqoCh--_Bgn7mB_A7U2w1S6HfMr9MqFCc4VMw7ls,12071
167
168
  wbfdm/import_export/handlers/instrument_list.py,sha256=mZRfpJFi6BhhrjH2qaFEPqqCK2ybg-DQm43Uck7G9_w,4864
168
169
  wbfdm/import_export/handlers/instrument_price.py,sha256=RbNTo78zZuttzlVFKxJrHcW7DRfcsta7QDEI8OiiDrA,3498
169
- wbfdm/import_export/handlers/option.py,sha256=DtqqdOMEA-u3jWVjmxRPKJ8miENj_t1k2DzAZEoOtXU,2384
170
+ wbfdm/import_export/handlers/option.py,sha256=MPzluMPJ3Yu7Ahmw9BA7-ssAbvPDdyca_rC-YVhU8bY,2378
170
171
  wbfdm/import_export/handlers/private_equities.py,sha256=tOx4lgQOB68lYTi3UzIPzDQsfay5k2pu5qv8jGzG030,1957
171
172
  wbfdm/import_export/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
172
173
  wbfdm/import_export/parsers/cbinsights/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -223,6 +224,7 @@ wbfdm/migrations/0028_instrumentprice_annualized_daily_volatility.py,sha256=pO5M
223
224
  wbfdm/migrations/0029_alter_instrumentprice_volume.py,sha256=0UFUwEaBcqiWjKw6un1gf8sluKCRRh9snDM4z4THDw8,510
224
225
  wbfdm/migrations/0030_alter_relatedinstrumentthroughmodel_related_type.py,sha256=10-89NB7-T7t3xFPpd4fYQkKejNR36UewIhe5_20QCo,565
225
226
  wbfdm/migrations/0031_exchange_apply_round_lot_size_and_more.py,sha256=MqcHxgJIt67BEuEYK8vnJHhx_cGFw9Ca9Az2EvsDy1o,863
227
+ wbfdm/migrations/0032_alter_instrumentprice_outstanding_shares.py,sha256=uRgkf6j97kNaXAo0-_V3XzHNE59hvxSruJB18g53YwU,570
226
228
  wbfdm/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
227
229
  wbfdm/models/__init__.py,sha256=PWsLtlKJFDYycCohPbjsRLeWi1xaxEkZbaoUKo0yOBU,96
228
230
  wbfdm/models/fields.py,sha256=eQ_6EnDBMy0U7WzE2DsdXIXOJH5dFiIN2VbO2Svw4R0,3942
@@ -235,18 +237,18 @@ wbfdm/models/exchanges/exchanges.py,sha256=RmM5shyyuxEGN2Y3JmeSWyU-SbpVARrvVFW72
235
237
  wbfdm/models/instruments/__init__.py,sha256=OvEkECJaCubBQC7B9yUrx15V982labvegeGXyEASVno,636
236
238
  wbfdm/models/instruments/classifications.py,sha256=EeZ_P8f1F1NfjUmLf9fDMF0iPM73qxQoArUfvjuCwHg,10906
237
239
  wbfdm/models/instruments/instrument_lists.py,sha256=GxfFyfYxEcJS36LAarHja49TOM8ffhIivpZX2-tPtZg,4234
238
- wbfdm/models/instruments/instrument_prices.py,sha256=4xDZ2ulwQ1grVuznchz3m3920LTmHkxWfiSLy-c2u0g,22306
240
+ wbfdm/models/instruments/instrument_prices.py,sha256=K7oMIz76WSrLmpNwcabThvtrP6WpBZZnrE9CHB5-UPQ,22345
239
241
  wbfdm/models/instruments/instrument_relationships.py,sha256=zpCZCnt5CqIg5bd6le_6TyirsSwGV2NaqTVKw3bd5vM,10660
240
242
  wbfdm/models/instruments/instrument_requests.py,sha256=XbpofRS8WHadHlTFjvXJyd0o7K9r2pzJtnpjVQZOLdI,7832
241
- wbfdm/models/instruments/instruments.py,sha256=N_g3yM5bajyo1tQBIeKkmQ4SdpPkUcEhKeEuTOJGwLw,44182
243
+ wbfdm/models/instruments/instruments.py,sha256=Tn9vIu0kys8pVVBiO58Z0Z5fAv0KWzkp4L4kjm7oh3c,44485
242
244
  wbfdm/models/instruments/options.py,sha256=hFprq7B5t4ctz8nVqzFsBEzftq_KDUSsSXl1zJyh7tE,7094
243
245
  wbfdm/models/instruments/private_equities.py,sha256=uzwZi8IkmCKAHVTxnuFya9tehx7kh57sTlTEi1ieDaM,2198
244
- wbfdm/models/instruments/querysets.py,sha256=zBY3lX_l0_gqIGjX4vkfn7DQ5QyF_okmIYZ6SV1Y6I4,7729
246
+ wbfdm/models/instruments/querysets.py,sha256=7r3pXNlpROkYgKc6gQH07RNeWX6jGeBAPUabUevE6Jo,11587
245
247
  wbfdm/models/instruments/utils.py,sha256=88jnWINSSC0OwH-mCEOPLZXuhBCtEsxBpSaZ38GteaE,1365
246
248
  wbfdm/models/instruments/llm/__init__.py,sha256=dSmxRmEWb0A4O_lUoWuRKt2mBtUuLCTPVVJqGyi_n40,52
247
249
  wbfdm/models/instruments/llm/create_instrument_news_relationships.py,sha256=f9MT-8cWYlexUfCkaOJa9erI9RaUNI-nqCEyf2tDkbA,3809
248
250
  wbfdm/models/instruments/mixin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
249
- wbfdm/models/instruments/mixin/financials_computed.py,sha256=L5wjXDsR0maiwfOKP6KyWNJNH4nGOoAjSc_hDM7fsj0,35105
251
+ wbfdm/models/instruments/mixin/financials_computed.py,sha256=E87I7O2WQgjY3zM3so4dzfExBzVtKTkTqnRjPwLHbyM,34920
250
252
  wbfdm/models/instruments/mixin/financials_serializer_fields.py,sha256=-OkpcUt1rZmB3nUcO2vckpJdVm8IxRqkPDEgcPqqoRU,68886
251
253
  wbfdm/models/instruments/mixin/instruments.py,sha256=fpt03q_Sq35R_ZmJpdcw81aA7wTocnp7ANE1jb-_cfI,10022
252
254
  wbfdm/serializers/__init__.py,sha256=AXb03RKHo6B0ts_IQOvx89n9wKG8pxiumYv9cr4EhvA,197
@@ -279,10 +281,11 @@ wbfdm/tests/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
279
281
  wbfdm/tests/models/test_classifications.py,sha256=f2aM9UyV54fkEncp-uewEdOc3_0D-iPMN5LwhIhJC9w,4979
280
282
  wbfdm/tests/models/test_exchanges.py,sha256=KwK278MpA3FkpVgjW2l2PIHL7e8uDur7dOzIaTQEwyw,138
281
283
  wbfdm/tests/models/test_instrument_list.py,sha256=UIxKgBd4U-T2I4WDZfwacgJ1nKwJWYX1HKhdQDpx1tA,4899
282
- wbfdm/tests/models/test_instrument_prices.py,sha256=ObqFbJxZveiOPAK9_kC_JO9VBNmZB6bvGM4BejsFJ3c,16633
284
+ wbfdm/tests/models/test_instrument_prices.py,sha256=dRaFGc3epG_N6p0KtDzXOH7Gkx_aCSUKNn3JZBhHr8M,15875
283
285
  wbfdm/tests/models/test_instruments.py,sha256=Vg2cYWAwdu00dziKDUe_MrTzrsygHaZQHnvibF8pvew,9784
284
286
  wbfdm/tests/models/test_merge.py,sha256=tXD5xIxZyZtXpm9WIQ4Yc8TQwsUnkxkKIvMNwaHOvgM,4632
285
287
  wbfdm/tests/models/test_options.py,sha256=DoEAHhNQE4kucpBRRm2S05ozabkERz-I4mUolsE2Qi8,2269
288
+ wbfdm/tests/models/test_queryset.py,sha256=bVNDU498vbh7ind9NbOzsI8TMv3Qe47fSMPzd58K0R4,4113
286
289
  wbfdm/viewsets/__init__.py,sha256=ZM29X0-5AkSH2rzF_gKUI4FoaEWCcMTXPjruJ4Gi_x8,383
287
290
  wbfdm/viewsets/esg.py,sha256=WrJaFxVUUFprGviF6GDMWuG3HQTXmyD-Ly1FRh7b11o,2867
288
291
  wbfdm/viewsets/exchanges.py,sha256=OAYOIKVVEihReeks2Pq6qcAafOxY4UL8l4TFzzr7Ckc,1785
@@ -350,7 +353,7 @@ wbfdm/viewsets/instruments/__init__.py,sha256=uydDdU6oZ6W2lgFkr3-cU7WZU7TeokXAA1
350
353
  wbfdm/viewsets/instruments/classifications.py,sha256=CMRTeI6hUClXzZUr7PeRWBXhT9fMiPiu-FvNP_jUQkM,11947
351
354
  wbfdm/viewsets/instruments/financials_analysis.py,sha256=xcVKR2H0P07NIyxFwPPPmVi3hWA0ZrfxOWMO8KjG6Ms,29202
352
355
  wbfdm/viewsets/instruments/instrument_lists.py,sha256=hwwHDNpHjjffxw08N_1LtkL5Fdi8c1Om-PLz6pTu4Ok,2878
353
- wbfdm/viewsets/instruments/instrument_prices.py,sha256=WGUe5WUl9PqulWSPCZgWcBWCmgxtu4lhm-UeAa4ngyg,24008
356
+ wbfdm/viewsets/instruments/instrument_prices.py,sha256=9mdNPU1D6ZFS5Bf0U1d3c6ZlYSCjrNMWv6Vhas3D8Ns,24075
354
357
  wbfdm/viewsets/instruments/instrument_requests.py,sha256=mmaARNl6ymDdlcLzcu16vVfFsunhtJkMw2r2NBfUp9U,1839
355
358
  wbfdm/viewsets/instruments/instruments.py,sha256=9sZIjvWFFQ6SRAal3OXhiqK8_JZJcKb7f_lGtiubqVE,4037
356
359
  wbfdm/viewsets/instruments/instruments_relationships.py,sha256=D2Ym84zXBFegQC9nMThKSWqSAcWa0WvcS_GXxM2po68,11076
@@ -359,6 +362,6 @@ wbfdm/viewsets/statements/__init__.py,sha256=odxtFYUDICPmz8WCE3nx93EvKZLSPBEI4d7
359
362
  wbfdm/viewsets/statements/statements.py,sha256=gA6RCI8-B__JwjEb6OZxpn8Y-9aF-YQ3HIQ7e1vfJMw,4304
360
363
  wbfdm/viewsets/technical_analysis/__init__.py,sha256=qtCIBg0uSiZeJq_1tEQFilnorMBkMe6uCMfqar6-cLE,77
361
364
  wbfdm/viewsets/technical_analysis/monthly_performances.py,sha256=O1j8CGfOranL74LqVvcf7jERaDIboEJZiBf_AbbVDQ8,3974
362
- wbfdm-1.54.20.dist-info/METADATA,sha256=g9v3GDmV3-oW4da7sToMGuYR5u3MeCKSAYQsxDy5EbE,769
363
- wbfdm-1.54.20.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
364
- wbfdm-1.54.20.dist-info/RECORD,,
365
+ wbfdm-1.55.4.dist-info/METADATA,sha256=o2OmieptL0acyD4PCfoJcnczSC3Fjaymeym5qN7UDqw,768
366
+ wbfdm-1.55.4.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
367
+ wbfdm-1.55.4.dist-info/RECORD,,