wbfdm 1.54.20__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.
- wbfdm/contrib/qa/dataloaders/market_data.py +1 -1
- wbfdm/models/instruments/instruments.py +6 -0
- wbfdm/models/instruments/querysets.py +83 -2
- wbfdm/tests/models/test_queryset.py +89 -0
- {wbfdm-1.54.20.dist-info → wbfdm-1.54.21.dist-info}/METADATA +1 -1
- {wbfdm-1.54.20.dist-info → wbfdm-1.54.21.dist-info}/RECORD +7 -6
- {wbfdm-1.54.20.dist-info → wbfdm-1.54.21.dist-info}/WHEEL +0 -0
|
@@ -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
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
363
|
-
wbfdm-1.54.
|
|
364
|
-
wbfdm-1.54.
|
|
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,,
|
|
File without changes
|