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.
- wbfdm/contrib/qa/dataloaders/fx_rates.py +49 -0
- wbfdm/contrib/qa/dataloaders/market_data.py +29 -36
- wbfdm/contrib/qa/dataloaders/statements.py +9 -2
- wbfdm/dataloaders/protocols.py +10 -0
- wbfdm/dataloaders/proxies.py +13 -1
- wbfdm/dataloaders/types.py +6 -0
- wbfdm/factories/instruments.py +2 -2
- wbfdm/import_export/handlers/option.py +2 -2
- wbfdm/migrations/0032_alter_instrumentprice_outstanding_shares.py +18 -0
- wbfdm/models/instruments/instrument_prices.py +3 -1
- wbfdm/models/instruments/instruments.py +6 -0
- wbfdm/models/instruments/mixin/financials_computed.py +0 -4
- wbfdm/models/instruments/querysets.py +82 -2
- wbfdm/tasks.py +4 -2
- wbfdm/tests/models/test_instrument_prices.py +0 -14
- wbfdm/tests/models/test_queryset.py +89 -0
- wbfdm/viewsets/instruments/instrument_prices.py +9 -9
- {wbfdm-1.54.20.dist-info → wbfdm-1.55.4.dist-info}/METADATA +1 -1
- {wbfdm-1.54.20.dist-info → wbfdm-1.55.4.dist-info}/RECORD +20 -17
- {wbfdm-1.54.20.dist-info → wbfdm-1.55.4.dist-info}/WHEEL +0 -0
|
@@ -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
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
271
|
+
try:
|
|
272
|
+
row["financial"] = Financial[RKDFinancial(row["external_code"]).name].value
|
|
273
|
+
except (ValueError, KeyError):
|
|
274
|
+
continue
|
|
268
275
|
yield row
|
wbfdm/dataloaders/protocols.py
CHANGED
|
@@ -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,
|
wbfdm/dataloaders/proxies.py
CHANGED
|
@@ -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
|
-
|
|
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)
|
wbfdm/dataloaders/types.py
CHANGED
|
@@ -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.
|
wbfdm/factories/instruments.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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.
|
|
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=
|
|
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/
|
|
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=
|
|
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=
|
|
122
|
-
wbfdm/dataloaders/proxies.py,sha256=
|
|
123
|
-
wbfdm/dataloaders/types.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
363
|
-
wbfdm-1.
|
|
364
|
-
wbfdm-1.
|
|
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,,
|
|
File without changes
|