wbfdm 1.54.19__py2.py3-none-any.whl → 1.54.21__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.

@@ -189,7 +189,7 @@ class DatastreamMarketDataDataloader(MarketDataProtocol, Dataloader):
189
189
 
190
190
  if MarketData.SHARES_OUTSTANDING in values:
191
191
  row["outstanding_shares"] = (row["market_capitalization"] / row["close"]) if row["close"] else None
192
-
192
+ row["fx_rate"] = row.get("fx_rate", 1.0)
193
193
  yield row
194
194
 
195
195
  cursor.execute(MSSQLQuery.drop_table(mapping).get_sql())
@@ -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:
@@ -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,80 @@ 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})"
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
+
252
+ if "fx_rate" in df.columns:
253
+ fx_rate_df = df["fx_rate"].fillna(1.0)
254
+ else:
255
+ fx_rate_df = pd.DataFrame(np.ones(prices_df.shape), index=prices_df.index, columns=prices_df.columns)
256
+ returns = prices_to_returns(fx_rate_df * prices_df, drop_inceptions_nan=False, fill_nan=True)
257
+ return {ts.date(): row for ts, row in prices_df.to_dict("index").items()}, returns.replace(
258
+ [np.inf, -np.inf, np.nan], 0
259
+ )
260
+ return {}, pd.DataFrame()
@@ -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
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbfdm
3
- Version: 1.54.19
3
+ Version: 1.54.21
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.*
@@ -100,7 +100,7 @@ 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/market_data.py,sha256=_B_QgoA_TAUK3KkHYsi1FM-K265wqiDCU-3ztA4mpiM,8374
104
104
  wbfdm/contrib/qa/dataloaders/officers.py,sha256=vytlQJJxmn4Y5HfNh5mHJAvuIrrsQSkNO-sONyhxftY,2940
105
105
  wbfdm/contrib/qa/dataloaders/reporting_dates.py,sha256=q25ccB0pbGfLJLV1A1_AY1XYWJ_Fa10egY09L1J-C5A,2628
106
106
  wbfdm/contrib/qa/dataloaders/statements.py,sha256=hC6YErJcvBTmaAmzscgeC4sBK3lYE2U5eIKRIE9b_cs,10094
@@ -238,10 +238,10 @@ wbfdm/models/instruments/instrument_lists.py,sha256=GxfFyfYxEcJS36LAarHja49TOM8f
238
238
  wbfdm/models/instruments/instrument_prices.py,sha256=4xDZ2ulwQ1grVuznchz3m3920LTmHkxWfiSLy-c2u0g,22306
239
239
  wbfdm/models/instruments/instrument_relationships.py,sha256=zpCZCnt5CqIg5bd6le_6TyirsSwGV2NaqTVKw3bd5vM,10660
240
240
  wbfdm/models/instruments/instrument_requests.py,sha256=XbpofRS8WHadHlTFjvXJyd0o7K9r2pzJtnpjVQZOLdI,7832
241
- wbfdm/models/instruments/instruments.py,sha256=N_g3yM5bajyo1tQBIeKkmQ4SdpPkUcEhKeEuTOJGwLw,44182
241
+ wbfdm/models/instruments/instruments.py,sha256=Tn9vIu0kys8pVVBiO58Z0Z5fAv0KWzkp4L4kjm7oh3c,44485
242
242
  wbfdm/models/instruments/options.py,sha256=hFprq7B5t4ctz8nVqzFsBEzftq_KDUSsSXl1zJyh7tE,7094
243
243
  wbfdm/models/instruments/private_equities.py,sha256=uzwZi8IkmCKAHVTxnuFya9tehx7kh57sTlTEi1ieDaM,2198
244
- wbfdm/models/instruments/querysets.py,sha256=zBY3lX_l0_gqIGjX4vkfn7DQ5QyF_okmIYZ6SV1Y6I4,7729
244
+ wbfdm/models/instruments/querysets.py,sha256=MNBep2F3G_MlbCu-IuzQIsxoRSWKjJBkj7FirNrWMT8,11533
245
245
  wbfdm/models/instruments/utils.py,sha256=88jnWINSSC0OwH-mCEOPLZXuhBCtEsxBpSaZ38GteaE,1365
246
246
  wbfdm/models/instruments/llm/__init__.py,sha256=dSmxRmEWb0A4O_lUoWuRKt2mBtUuLCTPVVJqGyi_n40,52
247
247
  wbfdm/models/instruments/llm/create_instrument_news_relationships.py,sha256=f9MT-8cWYlexUfCkaOJa9erI9RaUNI-nqCEyf2tDkbA,3809
@@ -283,6 +283,7 @@ wbfdm/tests/models/test_instrument_prices.py,sha256=ObqFbJxZveiOPAK9_kC_JO9VBNmZ
283
283
  wbfdm/tests/models/test_instruments.py,sha256=Vg2cYWAwdu00dziKDUe_MrTzrsygHaZQHnvibF8pvew,9784
284
284
  wbfdm/tests/models/test_merge.py,sha256=tXD5xIxZyZtXpm9WIQ4Yc8TQwsUnkxkKIvMNwaHOvgM,4632
285
285
  wbfdm/tests/models/test_options.py,sha256=DoEAHhNQE4kucpBRRm2S05ozabkERz-I4mUolsE2Qi8,2269
286
+ wbfdm/tests/models/test_queryset.py,sha256=bVNDU498vbh7ind9NbOzsI8TMv3Qe47fSMPzd58K0R4,4113
286
287
  wbfdm/viewsets/__init__.py,sha256=ZM29X0-5AkSH2rzF_gKUI4FoaEWCcMTXPjruJ4Gi_x8,383
287
288
  wbfdm/viewsets/esg.py,sha256=WrJaFxVUUFprGviF6GDMWuG3HQTXmyD-Ly1FRh7b11o,2867
288
289
  wbfdm/viewsets/exchanges.py,sha256=OAYOIKVVEihReeks2Pq6qcAafOxY4UL8l4TFzzr7Ckc,1785
@@ -359,6 +360,6 @@ wbfdm/viewsets/statements/__init__.py,sha256=odxtFYUDICPmz8WCE3nx93EvKZLSPBEI4d7
359
360
  wbfdm/viewsets/statements/statements.py,sha256=gA6RCI8-B__JwjEb6OZxpn8Y-9aF-YQ3HIQ7e1vfJMw,4304
360
361
  wbfdm/viewsets/technical_analysis/__init__.py,sha256=qtCIBg0uSiZeJq_1tEQFilnorMBkMe6uCMfqar6-cLE,77
361
362
  wbfdm/viewsets/technical_analysis/monthly_performances.py,sha256=O1j8CGfOranL74LqVvcf7jERaDIboEJZiBf_AbbVDQ8,3974
362
- wbfdm-1.54.19.dist-info/METADATA,sha256=m_H4naShgCSp0ZAMiu4TgQhxZP01jQbQM84GoAxxTCY,769
363
- wbfdm-1.54.19.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
364
- wbfdm-1.54.19.dist-info/RECORD,,
363
+ wbfdm-1.54.21.dist-info/METADATA,sha256=xy33eotFtUR8m6J-QSc42xtqstNMF-G4N2KG5ZjTKEA,769
364
+ wbfdm-1.54.21.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
365
+ wbfdm-1.54.21.dist-info/RECORD,,