wbfdm 1.49.5__py2.py3-none-any.whl → 1.59.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.
- wbfdm/admin/exchanges.py +1 -1
- wbfdm/admin/instruments.py +3 -2
- wbfdm/analysis/financial_analysis/change_point_detection.py +88 -0
- wbfdm/analysis/financial_analysis/statement_with_estimates.py +5 -6
- wbfdm/analysis/financial_analysis/utils.py +6 -0
- wbfdm/contrib/dsws/client.py +3 -3
- wbfdm/contrib/dsws/dataloaders/market_data.py +31 -3
- wbfdm/contrib/internal/dataloaders/market_data.py +43 -9
- wbfdm/contrib/metric/backends/base.py +2 -2
- wbfdm/contrib/metric/backends/statistics.py +47 -13
- wbfdm/contrib/metric/dispatch.py +3 -0
- wbfdm/contrib/metric/exceptions.py +1 -1
- wbfdm/contrib/metric/filters.py +19 -0
- wbfdm/contrib/metric/models.py +6 -0
- wbfdm/contrib/metric/orchestrators.py +4 -4
- wbfdm/contrib/metric/signals.py +7 -0
- wbfdm/contrib/metric/tasks.py +2 -3
- wbfdm/contrib/metric/viewsets/configs/display.py +2 -2
- wbfdm/contrib/metric/viewsets/mixins.py +6 -6
- wbfdm/contrib/msci/client.py +6 -2
- wbfdm/contrib/qa/database_routers.py +1 -1
- wbfdm/contrib/qa/dataloaders/adjustments.py +2 -1
- wbfdm/contrib/qa/dataloaders/corporate_actions.py +2 -1
- wbfdm/contrib/qa/dataloaders/financials.py +19 -1
- wbfdm/contrib/qa/dataloaders/fx_rates.py +86 -0
- wbfdm/contrib/qa/dataloaders/market_data.py +29 -40
- wbfdm/contrib/qa/dataloaders/officers.py +1 -1
- wbfdm/contrib/qa/dataloaders/statements.py +18 -3
- wbfdm/contrib/qa/jinja2/qa/sql/ibes/financials.sql +1 -1
- wbfdm/contrib/qa/sync/exchanges.py +2 -1
- wbfdm/contrib/qa/sync/utils.py +76 -17
- wbfdm/dataloaders/protocols.py +12 -1
- wbfdm/dataloaders/proxies.py +15 -1
- wbfdm/dataloaders/types.py +7 -1
- wbfdm/enums.py +2 -0
- wbfdm/factories/instruments.py +4 -2
- wbfdm/figures/financials/financial_analysis_charts.py +2 -8
- wbfdm/filters/classifications.py +2 -2
- wbfdm/filters/financials.py +9 -18
- wbfdm/filters/financials_analysis.py +36 -16
- wbfdm/filters/instrument_prices.py +8 -5
- wbfdm/filters/instruments.py +21 -7
- wbfdm/import_export/backends/cbinsights/utils/client.py +8 -8
- wbfdm/import_export/backends/refinitiv/utils/controller.py +1 -1
- wbfdm/import_export/handlers/instrument.py +160 -104
- wbfdm/import_export/handlers/option.py +2 -2
- wbfdm/import_export/parsers/cbinsights/equities.py +2 -3
- wbfdm/jinja2.py +2 -1
- wbfdm/locale/de/LC_MESSAGES/django.mo +0 -0
- wbfdm/locale/de/LC_MESSAGES/django.po +257 -0
- wbfdm/locale/en/LC_MESSAGES/django.mo +0 -0
- wbfdm/locale/en/LC_MESSAGES/django.po +255 -0
- wbfdm/locale/fr/LC_MESSAGES/django.mo +0 -0
- wbfdm/locale/fr/LC_MESSAGES/django.po +257 -0
- wbfdm/migrations/0031_exchange_apply_round_lot_size_and_more.py +23 -0
- wbfdm/migrations/0032_alter_instrumentprice_outstanding_shares.py +18 -0
- wbfdm/migrations/0033_alter_controversy_review.py +18 -0
- wbfdm/migrations/0034_alter_instrumentlist_instrument_list_type.py +18 -0
- wbfdm/models/esg/controversies.py +19 -23
- wbfdm/models/exchanges/exchanges.py +8 -4
- wbfdm/models/fields.py +2 -2
- wbfdm/models/fk_fields.py +3 -3
- wbfdm/models/instruments/instrument_lists.py +1 -0
- wbfdm/models/instruments/instrument_prices.py +8 -1
- wbfdm/models/instruments/instrument_relationships.py +3 -0
- wbfdm/models/instruments/instruments.py +139 -26
- wbfdm/models/instruments/llm/create_instrument_news_relationships.py +29 -22
- wbfdm/models/instruments/mixin/financials_computed.py +0 -4
- wbfdm/models/instruments/mixin/financials_serializer_fields.py +118 -118
- wbfdm/models/instruments/mixin/instruments.py +7 -4
- wbfdm/models/instruments/options.py +6 -0
- wbfdm/models/instruments/private_equities.py +3 -0
- wbfdm/models/instruments/querysets.py +138 -37
- wbfdm/models/instruments/utils.py +5 -0
- wbfdm/serializers/exchanges.py +1 -0
- wbfdm/serializers/instruments/__init__.py +1 -0
- wbfdm/serializers/instruments/instruments.py +9 -2
- wbfdm/serializers/instruments/mixins.py +3 -3
- wbfdm/tasks.py +13 -2
- wbfdm/tests/analysis/financial_analysis/test_statement_with_estimates.py +0 -1
- wbfdm/tests/models/test_instrument_prices.py +0 -14
- wbfdm/tests/models/test_instruments.py +21 -9
- wbfdm/tests/models/test_queryset.py +89 -0
- wbfdm/viewsets/configs/display/exchanges.py +1 -1
- wbfdm/viewsets/configs/display/financial_summary.py +2 -2
- wbfdm/viewsets/configs/display/instrument_prices.py +2 -70
- wbfdm/viewsets/configs/display/instruments.py +3 -4
- wbfdm/viewsets/configs/display/instruments_relationships.py +3 -1
- wbfdm/viewsets/configs/display/prices.py +1 -0
- wbfdm/viewsets/configs/display/statement_with_estimates.py +1 -2
- wbfdm/viewsets/configs/endpoints/classifications.py +0 -12
- wbfdm/viewsets/configs/endpoints/instrument_prices.py +4 -23
- wbfdm/viewsets/configs/titles/instrument_prices.py +2 -1
- wbfdm/viewsets/esg.py +2 -2
- wbfdm/viewsets/financial_analysis/financial_metric_analysis.py +2 -2
- wbfdm/viewsets/financial_analysis/financial_ratio_analysis.py +1 -1
- wbfdm/viewsets/financial_analysis/financial_summary.py +6 -6
- wbfdm/viewsets/financial_analysis/statement_with_estimates.py +7 -3
- wbfdm/viewsets/instruments/financials_analysis.py +9 -12
- wbfdm/viewsets/instruments/instrument_prices.py +9 -9
- wbfdm/viewsets/instruments/instruments.py +9 -7
- wbfdm/viewsets/instruments/utils.py +3 -3
- wbfdm/viewsets/market_data.py +1 -1
- wbfdm/viewsets/prices.py +5 -0
- wbfdm/viewsets/statements/statements.py +7 -3
- {wbfdm-1.49.5.dist-info → wbfdm-1.59.4.dist-info}/METADATA +2 -1
- {wbfdm-1.49.5.dist-info → wbfdm-1.59.4.dist-info}/RECORD +108 -95
- {wbfdm-1.49.5.dist-info → wbfdm-1.59.4.dist-info}/WHEEL +1 -1
- wbfdm/menu.py +0 -11
|
@@ -42,11 +42,14 @@ class InstrumentPMSMixin:
|
|
|
42
42
|
def get_price(self, val_date: date, price_date_timedelta: int = 3) -> Decimal:
|
|
43
43
|
if self.is_cash:
|
|
44
44
|
return Decimal(1)
|
|
45
|
-
return self._build_dto(val_date, price_date_timedelta=price_date_timedelta).close
|
|
45
|
+
return Decimal(self._build_dto(val_date, price_date_timedelta=price_date_timedelta).close)
|
|
46
46
|
|
|
47
47
|
def _build_dto(self, val_date: date, price_date_timedelta: int = 3) -> PriceDTO: # for backward compatibility
|
|
48
48
|
try:
|
|
49
|
-
|
|
49
|
+
try:
|
|
50
|
+
price = self.valuations.get(date=val_date)
|
|
51
|
+
except InstrumentPrice.DoesNotExist:
|
|
52
|
+
price = self.prices.get(date=val_date)
|
|
50
53
|
close = float(price.net_value)
|
|
51
54
|
return PriceDTO(
|
|
52
55
|
pk=price.id,
|
|
@@ -60,7 +63,7 @@ class InstrumentPMSMixin:
|
|
|
60
63
|
market_capitalization=price.market_capitalization,
|
|
61
64
|
outstanding_shares=float(price.outstanding_shares) if price.outstanding_shares else None,
|
|
62
65
|
)
|
|
63
|
-
except InstrumentPrice.DoesNotExist:
|
|
66
|
+
except InstrumentPrice.DoesNotExist as e:
|
|
64
67
|
prices = sorted(
|
|
65
68
|
self.get_prices(from_date=(val_date - BDay(price_date_timedelta)).date(), to_date=val_date),
|
|
66
69
|
key=lambda x: x["valuation_date"],
|
|
@@ -84,7 +87,7 @@ class InstrumentPMSMixin:
|
|
|
84
87
|
market_capitalization=p.get("market_capitalization", None),
|
|
85
88
|
outstanding_shares=p.get("outstanding_shares", None),
|
|
86
89
|
)
|
|
87
|
-
raise ValueError("Not price was found")
|
|
90
|
+
raise ValueError("Not price was found") from e
|
|
88
91
|
|
|
89
92
|
# Instrument Prices Utility Functions
|
|
90
93
|
@classmethod
|
|
@@ -143,6 +143,9 @@ class OptionAggregate(BaseOptionAbstractModel):
|
|
|
143
143
|
),
|
|
144
144
|
]
|
|
145
145
|
|
|
146
|
+
def __str__(self) -> str:
|
|
147
|
+
return f"{self.instrument} - {self.date} - {self.type}"
|
|
148
|
+
|
|
146
149
|
|
|
147
150
|
class Option(BaseOptionAbstractModel):
|
|
148
151
|
import_export_handler_class = OptionImportHandler
|
|
@@ -224,3 +227,6 @@ class Option(BaseOptionAbstractModel):
|
|
|
224
227
|
fields=["type"],
|
|
225
228
|
),
|
|
226
229
|
]
|
|
230
|
+
|
|
231
|
+
def __str__(self):
|
|
232
|
+
return f"{self.contract_identifier} - {self.date} - {self.type}"
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import logging
|
|
2
2
|
from contextlib import suppress
|
|
3
|
-
from datetime import date
|
|
3
|
+
from datetime import date, timedelta
|
|
4
|
+
from decimal import Decimal
|
|
4
5
|
|
|
5
6
|
import numpy as np
|
|
6
7
|
import pandas as pd
|
|
8
|
+
from django.core.exceptions import MultipleObjectsReturned, ValidationError
|
|
9
|
+
from django.core.validators import DecimalValidator
|
|
7
10
|
from django.db.models import (
|
|
8
11
|
AutoField,
|
|
12
|
+
Case,
|
|
9
13
|
Exists,
|
|
10
14
|
ExpressionWrapper,
|
|
11
15
|
F,
|
|
@@ -13,12 +17,17 @@ from django.db.models import (
|
|
|
13
17
|
Q,
|
|
14
18
|
QuerySet,
|
|
15
19
|
Subquery,
|
|
20
|
+
Value,
|
|
21
|
+
When,
|
|
16
22
|
)
|
|
17
23
|
from django.db.models.functions import Coalesce
|
|
18
|
-
from
|
|
24
|
+
from skfolio.preprocessing import prices_to_returns
|
|
25
|
+
from wbcore.contrib.currency.models import Currency, CurrencyFXRates
|
|
19
26
|
|
|
20
27
|
from wbfdm.enums import MarketData
|
|
21
28
|
|
|
29
|
+
logger = logging.getLogger("pms")
|
|
30
|
+
|
|
22
31
|
|
|
23
32
|
class InstrumentQuerySet(QuerySet):
|
|
24
33
|
def filter_active_at_date(self, val_date: date):
|
|
@@ -88,43 +97,49 @@ class InstrumentQuerySet(QuerySet):
|
|
|
88
97
|
from wbfdm.models import InstrumentPrice
|
|
89
98
|
|
|
90
99
|
def _dict_to_object(instrument, row):
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
close = row.get("close", None)
|
|
101
|
+
price_date = row.get("date")
|
|
102
|
+
if price_date and close is not None:
|
|
103
|
+
close = round(Decimal(close), 6)
|
|
104
|
+
# we validate that close can be inserting into our table<
|
|
105
|
+
with suppress(ValidationError):
|
|
106
|
+
validator = DecimalValidator(16, 6)
|
|
107
|
+
validator(close)
|
|
97
108
|
try:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
with suppress(CurrencyFXRates.DoesNotExist):
|
|
112
|
-
p = InstrumentPrice(
|
|
113
|
-
currency_fx_rate_to_usd=CurrencyFXRates.objects.get(
|
|
114
|
-
# we need to get the currency rate because we bulk create the object, and thus save is not called
|
|
115
|
-
date=price_date,
|
|
116
|
-
currency=instrument.currency,
|
|
117
|
-
),
|
|
118
|
-
instrument=instrument,
|
|
119
|
-
date=price_date,
|
|
120
|
-
calculated=row["calculated"],
|
|
121
|
-
net_value=close,
|
|
122
|
-
gross_value=close,
|
|
123
|
-
volume=row.get("volume", None),
|
|
124
|
-
market_capitalization=row.get("market_capitalization", None),
|
|
125
|
-
)
|
|
109
|
+
try:
|
|
110
|
+
InstrumentPrice.objects.get(instrument=instrument, date=price_date)
|
|
111
|
+
except MultipleObjectsReturned:
|
|
112
|
+
InstrumentPrice.objects.get(
|
|
113
|
+
instrument=instrument, date=price_date, calculated=False
|
|
114
|
+
).delete()
|
|
115
|
+
p = InstrumentPrice.objects.get(instrument=instrument, date=price_date)
|
|
116
|
+
p.net_value = close
|
|
117
|
+
p.gross_value = close
|
|
118
|
+
p.calculated = row["calculated"]
|
|
119
|
+
p.volume = row.get("volume", p.volume)
|
|
120
|
+
p.market_capitalization = row.get("market_capitalization", p.market_capitalization)
|
|
121
|
+
p.market_capitalization_consolidated = p.market_capitalization
|
|
126
122
|
p.set_dynamic_field(False)
|
|
123
|
+
p.id = None
|
|
127
124
|
return p
|
|
125
|
+
except InstrumentPrice.DoesNotExist:
|
|
126
|
+
with suppress(CurrencyFXRates.DoesNotExist):
|
|
127
|
+
p = InstrumentPrice(
|
|
128
|
+
currency_fx_rate_to_usd=CurrencyFXRates.objects.get(
|
|
129
|
+
# we need to get the currency rate because we bulk create the object, and thus save is not called
|
|
130
|
+
date=price_date,
|
|
131
|
+
currency=instrument.currency,
|
|
132
|
+
),
|
|
133
|
+
instrument=instrument,
|
|
134
|
+
date=price_date,
|
|
135
|
+
calculated=row["calculated"],
|
|
136
|
+
net_value=close,
|
|
137
|
+
gross_value=close,
|
|
138
|
+
volume=row.get("volume", None),
|
|
139
|
+
market_capitalization=row.get("market_capitalization", None),
|
|
140
|
+
)
|
|
141
|
+
p.set_dynamic_field(False)
|
|
142
|
+
return p
|
|
128
143
|
|
|
129
144
|
df = pd.DataFrame(
|
|
130
145
|
self.dl.market_data(
|
|
@@ -145,15 +160,101 @@ class InstrumentQuerySet(QuerySet):
|
|
|
145
160
|
|
|
146
161
|
for instrument_id, dff in df.groupby("instrument_id", group_keys=False, as_index=False):
|
|
147
162
|
dff = dff.drop(columns=["instrument_id"]).set_index("date").sort_index()
|
|
163
|
+
if dff.index.duplicated().any():
|
|
164
|
+
dff = dff.groupby(level=0).first()
|
|
165
|
+
logger.warning(
|
|
166
|
+
f"We detected a duplicated index for instrument id {instrument_id}. Please correct the dl parameter which likely introduced this issue."
|
|
167
|
+
)
|
|
168
|
+
|
|
148
169
|
dff = dff.reindex(pd.date_range(dff.index.min(), dff.index.max(), freq="B"))
|
|
170
|
+
|
|
149
171
|
dff[["close", "market_capitalization"]] = dff[["close", "market_capitalization"]].astype(float).ffill()
|
|
150
172
|
dff.volume = dff.volume.astype(float).fillna(0)
|
|
151
173
|
dff.calculated = dff.calculated.astype(bool).fillna(
|
|
152
174
|
True
|
|
153
175
|
) # we do not ffill calculated but set the to True to mark them as "estimated"/"not real"
|
|
176
|
+
|
|
154
177
|
dff = dff.reset_index(names="date").dropna(subset=["close"])
|
|
155
178
|
dff = dff.replace([np.inf, -np.inf, np.nan], None)
|
|
156
179
|
instrument = self.get(id=instrument_id)
|
|
180
|
+
|
|
157
181
|
yield from filter(
|
|
158
182
|
lambda x: x, map(lambda row: _dict_to_object(instrument, row), dff.to_dict("records"))
|
|
159
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
|
+
|
|
257
|
+
return {
|
|
258
|
+
ts.date(): row for ts, row in prices_df.replace([np.nan], None).to_dict("index").items()
|
|
259
|
+
}, returns.replace([np.inf, -np.inf, np.nan], 0)
|
|
260
|
+
return {}, pd.DataFrame()
|
|
@@ -40,3 +40,8 @@ def re_isin(input: str):
|
|
|
40
40
|
|
|
41
41
|
def re_mnemonic(input: str):
|
|
42
42
|
return set(re.findall(START_DELIMITER + r"([A-Z]+:[A-Z]+)" + END_DELIMITER, input))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def clean_ric(ric: str, exchange_code: str):
|
|
46
|
+
# Replace the matched exchange code with the new exchange
|
|
47
|
+
return re.sub(r"\.\w+$", f".{exchange_code}", ric)
|
wbfdm/serializers/exchanges.py
CHANGED
|
@@ -4,6 +4,7 @@ from .instruments import (
|
|
|
4
4
|
InstrumentRepresentationSerializer,
|
|
5
5
|
InstrumentTypeRepresentationSerializer,
|
|
6
6
|
InvestableUniverseRepresentationSerializer,
|
|
7
|
+
CompanyRepresentationSerializer,
|
|
7
8
|
ClassifiableInstrumentRepresentationSerializer,
|
|
8
9
|
SecurityRepresentationSerializer,
|
|
9
10
|
InvestableInstrumentRepresentationSerializer,
|
|
@@ -51,6 +51,13 @@ class ClassifiableInstrumentRepresentationSerializer(InstrumentRepresentationSer
|
|
|
51
51
|
return filter_params
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
class CompanyRepresentationSerializer(InstrumentRepresentationSerializer):
|
|
55
|
+
def get_filter_params(self, request):
|
|
56
|
+
filter_params = super().get_filter_params(request)
|
|
57
|
+
filter_params["level"] = 0
|
|
58
|
+
return filter_params
|
|
59
|
+
|
|
60
|
+
|
|
54
61
|
class SecurityRepresentationSerializer(InstrumentRepresentationSerializer):
|
|
55
62
|
def get_filter_params(self, request):
|
|
56
63
|
filter_params = super().get_filter_params(request)
|
|
@@ -89,14 +96,14 @@ class ManagedInstrumentRepresentationSerializer(InstrumentRepresentationSerializ
|
|
|
89
96
|
class EquityRepresentationSerializer(InstrumentRepresentationSerializer):
|
|
90
97
|
def get_filter_params(self, request):
|
|
91
98
|
filter_params = super().get_filter_params(request)
|
|
92
|
-
filter_params["
|
|
99
|
+
filter_params["instrument_type__key"] = "equity"
|
|
93
100
|
return filter_params
|
|
94
101
|
|
|
95
102
|
|
|
96
103
|
class ProductRepresentationSerializer(InstrumentRepresentationSerializer):
|
|
97
104
|
def get_filter_params(self, request):
|
|
98
105
|
filter_params = super().get_filter_params(request)
|
|
99
|
-
filter_params["
|
|
106
|
+
filter_params["instrument_type__key"] = "product"
|
|
100
107
|
return filter_params
|
|
101
108
|
|
|
102
109
|
|
|
@@ -29,9 +29,9 @@ class InstrumentAdditionalResourcesMixin:
|
|
|
29
29
|
args=[instance.id],
|
|
30
30
|
request=request,
|
|
31
31
|
)
|
|
32
|
-
additional_resources[
|
|
33
|
-
"
|
|
34
|
-
|
|
32
|
+
additional_resources["price_and_volume"] = (
|
|
33
|
+
f'{reverse("wbfdm:market_data-list", args=[instance.id], request=request)}?chart_type=close&indicators=sma_50,sma_100&volume=true'
|
|
34
|
+
)
|
|
35
35
|
|
|
36
36
|
additional_resources["classifications_list"] = reverse(
|
|
37
37
|
"wbfdm:instrument-classification-list",
|
wbfdm/tasks.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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
|
|
9
|
+
from wbcore.utils.cache import mapping
|
|
8
10
|
|
|
9
11
|
from wbfdm.models import Instrument, InstrumentPrice
|
|
10
12
|
from wbfdm.sync.runner import ( # noqa: F401
|
|
@@ -14,6 +16,7 @@ from wbfdm.sync.runner import ( # noqa: F401
|
|
|
14
16
|
synchronize_instruments,
|
|
15
17
|
)
|
|
16
18
|
|
|
19
|
+
from .contrib.metric.signals import instrument_metric_updated
|
|
17
20
|
from .signals import investable_universe_updated
|
|
18
21
|
|
|
19
22
|
|
|
@@ -51,7 +54,6 @@ def update_of_investable_universe_data(
|
|
|
51
54
|
Instrument.investable_universe.update(
|
|
52
55
|
is_investable_universe=True
|
|
53
56
|
) # ensure all the investable universe is marked as such
|
|
54
|
-
|
|
55
57
|
instruments = Instrument.active_objects.filter(is_investable_universe=True, delisted_date__isnull=True).exclude(
|
|
56
58
|
Q(is_managed=True)
|
|
57
59
|
| Q(dl_parameters__market_data__path="wbfdm.contrib.internal.dataloaders.market_data.MarketDataDataloader")
|
|
@@ -71,6 +73,7 @@ def update_instrument_metrics_as_task():
|
|
|
71
73
|
instruments = Instrument.active_objects.filter(is_investable_universe=True)
|
|
72
74
|
for instrument in tqdm(instruments, total=instruments.count()):
|
|
73
75
|
instrument.update_last_valuation_date()
|
|
76
|
+
instrument_metric_updated.send(sender=Instrument, basket=None, date=None, key=None)
|
|
74
77
|
|
|
75
78
|
|
|
76
79
|
@shared_task(queue="portfolio")
|
|
@@ -85,6 +88,14 @@ def synchronize_exchanges_as_task():
|
|
|
85
88
|
|
|
86
89
|
@shared_task(queue="portfolio")
|
|
87
90
|
def full_synchronization_as_task():
|
|
91
|
+
# we get all instrument without name or where we would expect a parent and consider them for clean up.
|
|
92
|
+
qs = Instrument.objects.filter(prices__isnull=True).filter(
|
|
93
|
+
(Q(name="") & Q(name_repr="")) | (Q(source__in=["qa-ds2-security", "qa-ds2-quote"]) & Q(parent__isnull=True))
|
|
94
|
+
)
|
|
95
|
+
for instrument in qs:
|
|
96
|
+
with suppress(ProtectedError):
|
|
97
|
+
instrument.delete()
|
|
98
|
+
mapping.cache_clear() # we need to clear the mapping cache because we might have deleted parent instruments
|
|
88
99
|
initialize_exchanges()
|
|
89
100
|
initialize_instruments()
|
|
90
101
|
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")
|
|
@@ -38,23 +38,25 @@ class TestInstrumentModel:
|
|
|
38
38
|
assert res[0]["market_capitalization"] == price.market_capitalization
|
|
39
39
|
assert res[0]["outstanding_shares"] == float(price.outstanding_shares)
|
|
40
40
|
|
|
41
|
-
def test_get_price(self, instrument_factory, instrument_price_factory):
|
|
41
|
+
def test_get_price(self, weekday, instrument_factory, instrument_price_factory):
|
|
42
42
|
instrument = instrument_factory.create()
|
|
43
43
|
other_instrument = instrument_factory.create()
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
price_calculated = instrument_price_factory.create(date=weekday, instrument=instrument, calculated=True)
|
|
45
|
+
|
|
46
46
|
instrument_price_factory.create(instrument=other_instrument) # Noise
|
|
47
|
-
assert instrument.get_price(
|
|
48
|
-
|
|
49
|
-
assert instrument.get_price(
|
|
50
|
-
assert instrument.get_price((
|
|
47
|
+
assert instrument.get_price(weekday) == float(price_calculated.net_value)
|
|
48
|
+
price_real = instrument_price_factory.create(date=weekday, instrument=instrument, calculated=False)
|
|
49
|
+
assert instrument.get_price(weekday) == float(price_real.net_value) # we prioritize real price
|
|
50
|
+
assert instrument.get_price((weekday + BDay(1)).date()) == float(price_real.net_value)
|
|
51
|
+
assert instrument.get_price((weekday + BDay(2)).date()) == float(price_real.net_value)
|
|
52
|
+
assert instrument.get_price((weekday + BDay(3)).date()) == float(price_real.net_value)
|
|
51
53
|
with pytest.raises(ValueError):
|
|
52
|
-
instrument.get_price((
|
|
54
|
+
instrument.get_price((weekday + BDay(4)).date()) # for return the latest valid price 3 days earlier.
|
|
53
55
|
|
|
54
56
|
# if the instrument is considered cash, we always return a value of 1
|
|
55
57
|
instrument.is_cash = True
|
|
56
58
|
instrument.save()
|
|
57
|
-
assert instrument.get_price(
|
|
59
|
+
assert instrument.get_price(weekday) == Decimal(1)
|
|
58
60
|
|
|
59
61
|
def test_extract_daily_performance_df(self):
|
|
60
62
|
tidx = pd.date_range("2016-07-01", periods=4, freq="B")
|
|
@@ -205,3 +207,13 @@ class TestInstrumentModel:
|
|
|
205
207
|
parent.save()
|
|
206
208
|
instrument.refresh_from_db()
|
|
207
209
|
assert instrument.name_repr == "test2"
|
|
210
|
+
|
|
211
|
+
def test_clean_ric(self, instrument_factory, exchange_factory):
|
|
212
|
+
exchange = exchange_factory.create(refinitiv_identifier_code=None)
|
|
213
|
+
instrument = instrument_factory.create(refinitiv_identifier_code="AAPL.AA", exchange=exchange)
|
|
214
|
+
assert instrument.refinitiv_identifier_code == "AAPL.AA"
|
|
215
|
+
|
|
216
|
+
exchange.refinitiv_identifier_code = "BB"
|
|
217
|
+
exchange.save()
|
|
218
|
+
instrument.save()
|
|
219
|
+
assert instrument.refinitiv_identifier_code == "AAPL.BB"
|
|
@@ -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
|
+
)
|
|
@@ -35,7 +35,7 @@ class ExchangeDisplayConfig(DisplayViewConfig):
|
|
|
35
35
|
[
|
|
36
36
|
["name", "mic_code", "operating_mic_code", "refinitiv_identifier_code"],
|
|
37
37
|
[".", "bbg_exchange_codes", "bbg_composite_primary", "bbg_composite"],
|
|
38
|
-
[
|
|
38
|
+
["country", "city", "website", "apply_round_lot_size"],
|
|
39
39
|
[repeat_field(2, "opening_time"), repeat_field(2, "closing_time")],
|
|
40
40
|
[repeat_field(4, "comments")],
|
|
41
41
|
]
|
|
@@ -80,7 +80,7 @@ class FinancialSummaryDisplayViewConfig(DisplayViewConfig):
|
|
|
80
80
|
label=col,
|
|
81
81
|
width=80,
|
|
82
82
|
formatting_rules=generate_formatting_rules(col),
|
|
83
|
-
|
|
83
|
+
suppress_auto_size=False,
|
|
84
84
|
resizable=False,
|
|
85
85
|
movable=False,
|
|
86
86
|
menu=False,
|
|
@@ -93,7 +93,7 @@ class FinancialSummaryDisplayViewConfig(DisplayViewConfig):
|
|
|
93
93
|
key="label",
|
|
94
94
|
label=" ",
|
|
95
95
|
width=120,
|
|
96
|
-
|
|
96
|
+
suppress_auto_size=False,
|
|
97
97
|
resizable=False,
|
|
98
98
|
movable=False,
|
|
99
99
|
menu=False,
|