wbfdm 1.43.2__py2.py3-none-any.whl → 1.44.0__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/analysis/financial_analysis/utils.py +3 -1
- wbfdm/contrib/qa/dataloaders/adjustments.py +54 -41
- wbfdm/contrib/qa/dataloaders/corporate_actions.py +47 -37
- wbfdm/contrib/qa/dataloaders/market_data.py +84 -59
- wbfdm/contrib/qa/dataloaders/officers.py +54 -39
- wbfdm/contrib/qa/dataloaders/utils.py +22 -0
- wbfdm/dataloaders/protocols.py +11 -18
- wbfdm/dataloaders/proxies.py +1 -1
- wbfdm/dataloaders/types.py +3 -0
- wbfdm/enums.py +3 -3
- wbfdm/serializers/instruments/instruments.py +1 -0
- wbfdm/tests/analysis/financial_analysis/test_statement_with_estimates.py +1 -1
- wbfdm/urls.py +1 -0
- wbfdm/viewsets/configs/buttons/instruments.py +1 -1
- wbfdm/viewsets/configs/display/__init__.py +1 -0
- wbfdm/viewsets/configs/display/financial_summary.py +133 -0
- wbfdm/viewsets/configs/display/instruments.py +11 -12
- wbfdm/viewsets/financial_analysis/__init__.py +1 -0
- wbfdm/viewsets/financial_analysis/financial_summary.py +256 -0
- wbfdm/viewsets/mixins.py +2 -0
- {wbfdm-1.43.2.dist-info → wbfdm-1.44.0.dist-info}/METADATA +3 -1
- {wbfdm-1.43.2.dist-info → wbfdm-1.44.0.dist-info}/RECORD +23 -20
- {wbfdm-1.43.2.dist-info → wbfdm-1.44.0.dist-info}/WHEEL +0 -0
|
@@ -25,6 +25,7 @@ class Loader:
|
|
|
25
25
|
calendar_type: CalendarType = CalendarType.FISCAL,
|
|
26
26
|
market_data_values: list[MarketData] | None = None,
|
|
27
27
|
statement_values: list[Financial] | None = None,
|
|
28
|
+
period_type: PeriodType = PeriodType.ALL,
|
|
28
29
|
):
|
|
29
30
|
self.instrument = instrument
|
|
30
31
|
self.calendar_type = calendar_type
|
|
@@ -35,6 +36,7 @@ class Loader:
|
|
|
35
36
|
self.statement_values = (
|
|
36
37
|
statement_values # specify if any extra statement needs to be merged into the dataframe
|
|
37
38
|
)
|
|
39
|
+
self.period_type = period_type
|
|
38
40
|
self.errors: dict[str, list[str]] = defaultdict(list)
|
|
39
41
|
|
|
40
42
|
def load(self) -> pd.DataFrame:
|
|
@@ -54,9 +56,9 @@ class Loader:
|
|
|
54
56
|
df = pd.DataFrame(
|
|
55
57
|
Instrument.objects.filter(id=self.instrument.id).dl.financials(
|
|
56
58
|
values=self.values,
|
|
57
|
-
period_type=PeriodType.ALL,
|
|
58
59
|
from_year=date.today().year - 5,
|
|
59
60
|
calendar_type=self.calendar_type,
|
|
61
|
+
period_type=self.period_type,
|
|
60
62
|
)
|
|
61
63
|
)
|
|
62
64
|
if df.empty:
|
|
@@ -1,56 +1,69 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
1
2
|
from datetime import date
|
|
2
|
-
from itertools import batched
|
|
3
|
+
from itertools import batched
|
|
3
4
|
from typing import Iterator
|
|
4
5
|
|
|
5
|
-
from django.db import connections
|
|
6
|
-
from jinjasql import JinjaSql # type: ignore
|
|
6
|
+
from django.db import ProgrammingError, connections
|
|
7
7
|
from wbcore.contrib.dataloader.dataloaders import Dataloader
|
|
8
8
|
from wbcore.contrib.dataloader.utils import dictfetchall
|
|
9
|
+
from wbfdm.contrib.qa.dataloaders.utils import SOURCE_DS2
|
|
9
10
|
from wbfdm.dataloaders.protocols import AdjustmentsProtocol
|
|
10
11
|
from wbfdm.dataloaders.types import AdjustmentDataDict
|
|
11
12
|
|
|
13
|
+
import pypika as pk
|
|
14
|
+
from pypika import functions as fn
|
|
15
|
+
from pypika.enums import SqlTypes, Order
|
|
16
|
+
from pypika.terms import LiteralValue
|
|
17
|
+
|
|
12
18
|
|
|
13
19
|
class DatastreamAdjustmentsDataloader(AdjustmentsProtocol, Dataloader):
|
|
14
|
-
def adjustments(self, from_date: date, to_date: date) -> Iterator[AdjustmentDataDict]:
|
|
20
|
+
def adjustments(self, from_date: date | None = None, to_date: date | None = None) -> Iterator[AdjustmentDataDict]:
|
|
15
21
|
lookup = {k: v for k, v in self.entities.values_list("dl_parameters__adjustments__parameters", "id")}
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
InfoCode = {{instrument}} {% if not loop.last %} OR {% endif %}
|
|
33
|
-
{% endfor %}
|
|
34
|
-
)
|
|
35
|
-
{% if from_date %} AND AdjDate >= {{ from_date }} {% endif %}
|
|
36
|
-
{% if to_date %} AND AdjDate <= {{ to_date }} {% endif %}
|
|
37
|
-
|
|
38
|
-
ORDER BY AdjDate DESC
|
|
39
|
-
"""
|
|
40
|
-
for batch in batched(lookup.keys(), 1000):
|
|
41
|
-
query, bind_params = JinjaSql(param_style="format").prepare_query(
|
|
42
|
-
sql,
|
|
43
|
-
{
|
|
44
|
-
"instruments": batch,
|
|
45
|
-
"from_date": from_date,
|
|
46
|
-
"to_date": to_date,
|
|
47
|
-
},
|
|
23
|
+
adj = pk.Table("DS2Adj")
|
|
24
|
+
adj_date = fn.Cast(adj.AdjDate, SqlTypes.DATE).as_("adjustment_date")
|
|
25
|
+
adj_end_date = fn.Cast(adj.EndAdjDate, SqlTypes.DATE).as_("adjustment_end_date")
|
|
26
|
+
|
|
27
|
+
infocode = pk.Table("#ds2infocode")
|
|
28
|
+
|
|
29
|
+
query = (
|
|
30
|
+
pk.MSSQLQuery.select(
|
|
31
|
+
adj.InfoCode.as_("external_identifier"),
|
|
32
|
+
fn.Concat(adj.InfoCode, "_", adj_date).as_("id"),
|
|
33
|
+
adj_date,
|
|
34
|
+
adj_end_date,
|
|
35
|
+
SOURCE_DS2,
|
|
36
|
+
adj.AdjFactor.as_("adjustment_factor"),
|
|
37
|
+
adj.CumAdjFactor.as_("cumulative_adjustement_factor"),
|
|
48
38
|
)
|
|
49
|
-
|
|
39
|
+
.from_(adj)
|
|
40
|
+
.where(adj.AdjType == 2)
|
|
41
|
+
.where(adj.InfoCode.isin([LiteralValue("select infocode from #ds2infocode")]))
|
|
42
|
+
.orderby(adj.AdjDate, order=Order.desc)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if from_date:
|
|
46
|
+
query = query.where(adj.AdjDate >= from_date)
|
|
47
|
+
|
|
48
|
+
if to_date:
|
|
49
|
+
query = query.where(adj.AdjDate <= to_date)
|
|
50
|
+
|
|
51
|
+
with connections["qa"].cursor() as cursor:
|
|
52
|
+
# we suppress an error here, because if the temporary table already exists
|
|
53
|
+
# then we do not want to fail. It should not fail, but if a previous run did
|
|
54
|
+
# not clean up the table properly, then at least we do not get stuck here
|
|
55
|
+
with suppress(ProgrammingError):
|
|
50
56
|
cursor.execute(
|
|
51
|
-
|
|
52
|
-
bind_params,
|
|
57
|
+
pk.MSSQLQuery.create_table(infocode).columns(pk.Column("infocode", SqlTypes.INTEGER)).get_sql()
|
|
53
58
|
)
|
|
54
|
-
for
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
for batch in batched(lookup.keys(), 1000):
|
|
60
|
+
cursor.execute(f"insert into #ds2infocode values {",".join(map(lambda x: f"({x})", batch))};")
|
|
61
|
+
|
|
62
|
+
cursor.execute(query.get_sql())
|
|
63
|
+
|
|
64
|
+
for row in dictfetchall(cursor, AdjustmentDataDict):
|
|
65
|
+
row["instrument_id"] = lookup[row["external_identifier"]]
|
|
66
|
+
yield row
|
|
67
|
+
|
|
68
|
+
# here we remove the temporary table again to avoid data spillage
|
|
69
|
+
# cursor.execute(pk.MSSQLQuery.drop_table(infocode).get_sql())
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
1
2
|
from datetime import date
|
|
3
|
+
from itertools import batched
|
|
2
4
|
from typing import Iterator
|
|
3
5
|
|
|
4
|
-
from django.db import connections
|
|
5
|
-
from jinjasql import JinjaSql # type: ignore
|
|
6
|
+
from django.db import ProgrammingError, connections
|
|
6
7
|
from wbcore.contrib.dataloader.dataloaders import Dataloader
|
|
7
8
|
from wbcore.contrib.dataloader.utils import dictfetchall
|
|
9
|
+
from wbfdm.contrib.qa.dataloaders.utils import SOURCE_DS2
|
|
8
10
|
from wbfdm.dataloaders.protocols import CorporateActionsProtocol
|
|
9
11
|
from wbfdm.dataloaders.types import CorporateActionDataDict
|
|
10
12
|
|
|
13
|
+
import pypika as pk
|
|
14
|
+
from pypika import functions as fn
|
|
15
|
+
from pypika.enums import SqlTypes
|
|
16
|
+
from pypika.terms import LiteralValue
|
|
17
|
+
|
|
11
18
|
|
|
12
19
|
class DatastreamCorporateActionsDataloader(CorporateActionsProtocol, Dataloader):
|
|
13
20
|
def corporate_actions(
|
|
@@ -17,43 +24,46 @@ class DatastreamCorporateActionsDataloader(CorporateActionsProtocol, Dataloader)
|
|
|
17
24
|
) -> Iterator[CorporateActionDataDict]:
|
|
18
25
|
lookup = {k: v for k, v in self.entities.values_list("dl_parameters__corporate_actions__parameters", "id")}
|
|
19
26
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
InfoCode = {{instrument}} {% if not loop.last %} OR {% endif %}
|
|
36
|
-
{% endfor %}
|
|
27
|
+
cap_event = pk.Table("Ds2CapEvent")
|
|
28
|
+
effective_date = fn.Cast(cap_event.EffectiveDate, SqlTypes.DATE)
|
|
29
|
+
infocode = pk.Table("#ds2infocode")
|
|
30
|
+
|
|
31
|
+
query = (
|
|
32
|
+
pk.MSSQLQuery.select(
|
|
33
|
+
cap_event.InfoCode.as_("external_identifier"),
|
|
34
|
+
fn.Concat(cap_event.InfoCode, "_", effective_date).as_("id"),
|
|
35
|
+
effective_date.as_("valuation_date"),
|
|
36
|
+
SOURCE_DS2,
|
|
37
|
+
cap_event.ActionTypeCode.as_("action_code"),
|
|
38
|
+
cap_event.EventStatusCode.as_("event_code"),
|
|
39
|
+
cap_event.NumOldShares.as_("old_shares"),
|
|
40
|
+
cap_event.NumNewShares.as_("new_shares"),
|
|
41
|
+
cap_event.ISOCurrCode.as_("currency"),
|
|
37
42
|
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
ORDER BY EffectiveDate DESC
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
query, bind_params = JinjaSql(param_style="format").prepare_query(
|
|
45
|
-
sql,
|
|
46
|
-
{
|
|
47
|
-
"instruments": self.entities.values_list("dl_parameters__corporate_actions__parameters", flat=True),
|
|
48
|
-
"from_date": from_date,
|
|
49
|
-
"to_date": to_date,
|
|
50
|
-
},
|
|
43
|
+
.from_(cap_event)
|
|
44
|
+
.where(cap_event.InfoCode.isin([LiteralValue("select infocode from #ds2infocode")]))
|
|
45
|
+
.orderby(cap_event.EffectiveDate, order=pk.Order.desc)
|
|
51
46
|
)
|
|
47
|
+
|
|
48
|
+
if from_date:
|
|
49
|
+
query = query.where(cap_event.EffectiveDate >= from_date)
|
|
50
|
+
|
|
51
|
+
if to_date:
|
|
52
|
+
query = query.where(cap_event.EffectiveDate <= to_date)
|
|
53
|
+
|
|
52
54
|
with connections["qa"].cursor() as cursor:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
# Create temporary table if it doesn't exist
|
|
56
|
+
with suppress(ProgrammingError):
|
|
57
|
+
cursor.execute(
|
|
58
|
+
pk.MSSQLQuery.create_table(infocode).columns(pk.Column("infocode", SqlTypes.INTEGER)).get_sql()
|
|
59
|
+
)
|
|
60
|
+
for batch in batched(lookup.keys(), 1000):
|
|
61
|
+
cursor.execute(f"insert into #ds2infocode values {','.join(map(lambda x: f'({x})', batch))};")
|
|
62
|
+
|
|
63
|
+
cursor.execute(query.get_sql())
|
|
64
|
+
for row in dictfetchall(cursor, CorporateActionDataDict):
|
|
58
65
|
row["instrument_id"] = lookup[row["external_identifier"]]
|
|
59
66
|
yield row
|
|
67
|
+
|
|
68
|
+
# Clean up temporary table
|
|
69
|
+
cursor.execute(pk.MSSQLQuery.drop_table(infocode).get_sql())
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from functools import reduce
|
|
3
|
+
from itertools import batched
|
|
1
4
|
from datetime import date
|
|
2
5
|
from enum import Enum
|
|
3
6
|
from typing import Iterator
|
|
7
|
+
import pypika as pk
|
|
8
|
+
from pypika import Column, MSSQLQuery, functions as fn
|
|
9
|
+
from pypika.enums import SqlTypes, Order
|
|
10
|
+
from pypika.terms import ValueWrapper, LiteralValue
|
|
4
11
|
|
|
5
|
-
from django.db import connections
|
|
6
|
-
from jinjasql import JinjaSql # type: ignore
|
|
12
|
+
from django.db import ProgrammingError, connections
|
|
7
13
|
from wbcore.contrib.dataloader.dataloaders import Dataloader
|
|
8
14
|
from wbcore.contrib.dataloader.utils import dictfetchall
|
|
9
15
|
from wbfdm.dataloaders.protocols import MarketDataProtocol
|
|
16
|
+
from wbfdm.contrib.qa.dataloaders.utils import create_table
|
|
10
17
|
from wbfdm.dataloaders.types import MarketDataDict
|
|
11
18
|
from wbfdm.enums import Frequency, MarketData
|
|
12
19
|
|
|
@@ -53,65 +60,83 @@ class DatastreamMarketDataDataloader(MarketDataProtocol, Dataloader):
|
|
|
53
60
|
}
|
|
54
61
|
value_mapping = [(DS2MarketData[x.name].value, x.value) for x in values or []]
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
'qa-ds2' as source,
|
|
62
|
-
{% if target_currency %}
|
|
63
|
-
'{{ target_currency|sqlsafe }}' as 'currency',
|
|
64
|
-
{% else %}
|
|
65
|
-
Currency as 'currency',
|
|
66
|
-
{% endif %}
|
|
67
|
-
{% for value in values %}
|
|
68
|
-
{% if target_currency %}
|
|
69
|
-
COALESCE(fx_rate.midrate, 1) *
|
|
70
|
-
{% endif %}
|
|
71
|
-
{{ value[0][0] | sqlsafe }}{% if value[0][1] %} * {{ value[0][1] | sqlsafe }}{% endif %} as '{{ value[1] | sqlsafe }}'{% if not loop.last %}, {% endif %}
|
|
72
|
-
{% endfor %}
|
|
73
|
-
FROM vw_Ds2Pricing as pricing
|
|
74
|
-
LEFT JOIN DS2MktVal as market_val
|
|
75
|
-
ON pricing.InfoCode = market_val.InfoCode
|
|
76
|
-
AND pricing.MarketDate = market_val.ValDate
|
|
77
|
-
{% if target_currency %}
|
|
78
|
-
LEFT JOIN Ds2FxCode as fx_code
|
|
79
|
-
ON fx_code.FromCurrCode = '{{target_currency|sqlsafe}}'
|
|
80
|
-
AND fx_code.ToCurrCode = pricing.Currency
|
|
81
|
-
AND fx_code.RateTypeCode = 'SPOT'
|
|
82
|
-
LEFT JOIN Ds2FxRate as fx_rate
|
|
83
|
-
ON fx_rate.ExRateIntCode = fx_code.ExRateIntCode
|
|
84
|
-
AND fx_rate.ExRateDate = pricing.MarketDate
|
|
85
|
-
{% endif %}
|
|
86
|
-
|
|
87
|
-
WHERE (
|
|
88
|
-
{% for instrument in instruments %}
|
|
89
|
-
(pricing.InfoCode = {{instrument[0]}} AND ExchIntCode = {{instrument[1]}}) {% if not loop.last %} OR {% endif %}
|
|
90
|
-
{% endfor %}
|
|
91
|
-
)
|
|
92
|
-
AND AdjType = 2
|
|
93
|
-
{% if from_date %} AND MarketDate >= {{ from_date }} {% endif %}
|
|
94
|
-
{% if to_date %} AND MarketDate <= {{ to_date }} {% endif %}
|
|
95
|
-
{% if exact_date %} AND MarketDate = {{ exact_date }} {% endif %}
|
|
63
|
+
# Define tables
|
|
64
|
+
pricing = pk.Table("vw_DS2Pricing")
|
|
65
|
+
market_val = pk.Table("DS2MktVal")
|
|
66
|
+
fx_code = pk.Table("DS2FxCode")
|
|
67
|
+
fx = pk.Table("DS2FxRate")
|
|
96
68
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
query, bind_params = JinjaSql(param_style="format").prepare_query(
|
|
100
|
-
sql,
|
|
101
|
-
{
|
|
102
|
-
"instruments": self.entities.values_list("dl_parameters__market_data__parameters", flat=True),
|
|
103
|
-
"values": value_mapping,
|
|
104
|
-
"from_date": from_date,
|
|
105
|
-
"to_date": to_date,
|
|
106
|
-
"exact_date": exact_date,
|
|
107
|
-
"target_currency": target_currency,
|
|
108
|
-
},
|
|
69
|
+
mapping, create_mapping_table = create_table(
|
|
70
|
+
"#ds2infoexchcode", Column("InfoCode", SqlTypes.INTEGER), Column("ExchIntCode", SqlTypes.INTEGER)
|
|
109
71
|
)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
72
|
+
|
|
73
|
+
# Base query to get data we always need unconditionally
|
|
74
|
+
query = (
|
|
75
|
+
pk.MSSQLQuery.from_(pricing)
|
|
76
|
+
.select(
|
|
77
|
+
fn.Concat(pricing.InfoCode, ",", pricing.ExchIntCode).as_("external_identifier"),
|
|
78
|
+
fn.Concat(
|
|
79
|
+
pricing.InfoCode, ",", pricing.ExchIntCode, "_", fn.Cast(pricing.MarketDate, SqlTypes.DATE)
|
|
80
|
+
).as_("id"),
|
|
81
|
+
fn.Cast(pricing.MarketDate, SqlTypes.DATE).as_("valuation_date"),
|
|
82
|
+
ValueWrapper("qa-ds2").as_("source"),
|
|
83
|
+
)
|
|
84
|
+
.left_join(market_val)
|
|
85
|
+
.on((pricing.InfoCode == market_val.InfoCode) & (pricing.MarketDate == market_val.ValDate))
|
|
86
|
+
# We join on _codes, which removes all instruments not in _codes - implicit where
|
|
87
|
+
.join(mapping)
|
|
88
|
+
.on((pricing.InfoCode == mapping.InfoCode) & (pricing.ExchIntCode == mapping.ExchIntCode))
|
|
89
|
+
.where(pricing.AdjType == 2)
|
|
90
|
+
.orderby(pricing.MarketDate, order=Order.desc)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# If we need to convert to a target currency, we need the fx rate table and multiply all values with the fx rate
|
|
94
|
+
if target_currency:
|
|
95
|
+
query = (
|
|
96
|
+
query.select(
|
|
97
|
+
ValueWrapper(target_currency).as_("currency"),
|
|
98
|
+
*[
|
|
99
|
+
(LiteralValue(value[0][0]) * fn.Coalesce(fx.midrate, 1)).as_(value[1])
|
|
100
|
+
for value in value_mapping
|
|
101
|
+
],
|
|
102
|
+
)
|
|
103
|
+
.left_join(fx_code)
|
|
104
|
+
.on(
|
|
105
|
+
(fx_code.FromCurrCode == target_currency)
|
|
106
|
+
& (fx_code.ToCurrCode == pricing.Currency)
|
|
107
|
+
& (fx_code.RateTypeCode == "SPOT")
|
|
108
|
+
)
|
|
109
|
+
.left_join(fx)
|
|
110
|
+
.on((fx_code.ExRateIntCode == fx.ExRateIntCode) & (fx.ExRateDate == pricing.MarketDate))
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
query = query.select(
|
|
114
|
+
pricing.Currency.as_("currency"),
|
|
115
|
+
*[LiteralValue(value[0][0]).as_(value[1]) for value in value_mapping],
|
|
114
116
|
)
|
|
115
|
-
|
|
117
|
+
|
|
118
|
+
# Add conditional where clauses
|
|
119
|
+
if from_date:
|
|
120
|
+
query = query.where(pricing.MarketDate >= from_date)
|
|
121
|
+
|
|
122
|
+
if to_date:
|
|
123
|
+
query = query.where(pricing.MarketDate <= to_date)
|
|
124
|
+
|
|
125
|
+
if exact_date:
|
|
126
|
+
query = query.where(pricing.MarketDate == exact_date)
|
|
127
|
+
|
|
128
|
+
with connections["qa"].cursor() as cursor:
|
|
129
|
+
with suppress(ProgrammingError):
|
|
130
|
+
cursor.execute(create_mapping_table.get_sql())
|
|
131
|
+
for batch in batched(
|
|
132
|
+
self.entities.values_list("dl_parameters__market_data__parameters", flat=True), 1000
|
|
133
|
+
):
|
|
134
|
+
cursor.execute(reduce(lambda x, y: x.insert(y), batch, MSSQLQuery.into(mapping)).get_sql())
|
|
135
|
+
|
|
136
|
+
cursor.execute(query.get_sql())
|
|
137
|
+
|
|
138
|
+
for row in dictfetchall(cursor, MarketDataDict):
|
|
116
139
|
row["instrument_id"] = lookup[row["external_identifier"]]
|
|
117
140
|
yield row
|
|
141
|
+
|
|
142
|
+
cursor.execute(MSSQLQuery.drop_table(mapping).get_sql())
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
from typing import Iterator
|
|
2
2
|
|
|
3
|
-
from django.db import connections
|
|
4
|
-
from jinjasql import JinjaSql # type: ignore
|
|
3
|
+
from django.db import connections, ProgrammingError
|
|
5
4
|
from wbcore.contrib.dataloader.dataloaders import Dataloader
|
|
6
5
|
from wbcore.contrib.dataloader.utils import dictfetchall
|
|
7
6
|
from wbfdm.dataloaders.protocols import OfficersProtocol
|
|
8
7
|
from wbfdm.dataloaders.types import OfficerDataDict
|
|
8
|
+
from itertools import batched
|
|
9
|
+
from contextlib import suppress
|
|
10
|
+
|
|
11
|
+
import pypika as pk
|
|
12
|
+
from pypika import functions as fn
|
|
13
|
+
from pypika.enums import SqlTypes
|
|
14
|
+
from pypika.terms import LiteralValue
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
from pypika.analytics import RowNumber
|
|
9
18
|
|
|
10
19
|
|
|
11
20
|
class RKDOfficersDataloader(OfficersProtocol, Dataloader):
|
|
@@ -14,46 +23,52 @@ class RKDOfficersDataloader(OfficersProtocol, Dataloader):
|
|
|
14
23
|
) -> Iterator[OfficerDataDict]:
|
|
15
24
|
lookup = {k: v for k, v in self.entities.values_list("dl_parameters__officers__parameters", "id")}
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
# Define tables
|
|
27
|
+
designation = pk.Table("RKDFndCmpOffTitleChg")
|
|
28
|
+
officer = pk.Table("RKDFndCmpOfficer")
|
|
29
|
+
temp_codes = pk.Table("#rkd_codes")
|
|
30
|
+
|
|
31
|
+
# Build the query
|
|
32
|
+
query = (
|
|
33
|
+
pk.MSSQLQuery.select(
|
|
34
|
+
fn.Concat(designation.Code, "-", RowNumber().orderby(officer.OfficerRank)).as_("id"),
|
|
35
|
+
designation.Code.as_("external_identifier"),
|
|
36
|
+
designation.Title.as_("position"),
|
|
37
|
+
fn.Concat(
|
|
23
38
|
officer.Prefix,
|
|
24
|
-
|
|
39
|
+
" ",
|
|
25
40
|
officer.FirstName,
|
|
26
|
-
|
|
41
|
+
" ",
|
|
27
42
|
officer.LastName,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
officer.Age as age,
|
|
34
|
-
officer.Sex as sex,
|
|
35
|
-
CONVERT(DATE, designation.DesgStartDt) as start
|
|
36
|
-
FROM RKDFndCmpOffTitleChg AS designation
|
|
37
|
-
JOIN RKDFndCmpOfficer AS officer
|
|
38
|
-
ON designation.Code = officer.Code
|
|
39
|
-
AND designation.OfficerID = officer.Officerid
|
|
40
|
-
|
|
41
|
-
WHERE
|
|
42
|
-
designation.Code in (
|
|
43
|
-
{% for instrument in instruments %}
|
|
44
|
-
{{instrument}} {% if not loop.last %}, {% endif %}
|
|
45
|
-
{% endfor %})
|
|
46
|
-
AND DesgEndDt IS NULL
|
|
47
|
-
|
|
48
|
-
ORDER BY
|
|
49
|
-
officer.OfficerRank
|
|
50
|
-
"""
|
|
51
|
-
query, bind_params = JinjaSql(param_style="format").prepare_query(sql, {"instruments": lookup.keys()})
|
|
52
|
-
with connections["qa"].cursor() as cursor:
|
|
53
|
-
cursor.execute(
|
|
54
|
-
query,
|
|
55
|
-
bind_params,
|
|
43
|
+
pk.Case().when(officer.Suffix.isnull(), "").else_(fn.Concat(", ", officer.Suffix)),
|
|
44
|
+
).as_("name"),
|
|
45
|
+
officer.Age.as_("age"),
|
|
46
|
+
officer.Sex.as_("sex"),
|
|
47
|
+
fn.Cast(designation.DesgStartDt, SqlTypes.DATE).as_("start"),
|
|
56
48
|
)
|
|
57
|
-
|
|
49
|
+
.from_(designation)
|
|
50
|
+
.join(officer)
|
|
51
|
+
.on((designation.Code == officer.Code) & (designation.OfficerID == officer.Officerid))
|
|
52
|
+
.where(designation.Code.isin([LiteralValue("select code from #rkd_codes")]))
|
|
53
|
+
.where(designation.DesgEndDt.isnull())
|
|
54
|
+
.orderby(officer.OfficerRank)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
with connections["qa"].cursor() as cursor:
|
|
58
|
+
# Create and populate temporary table
|
|
59
|
+
with suppress(ProgrammingError):
|
|
60
|
+
cursor.execute(
|
|
61
|
+
pk.MSSQLQuery.create_table(temp_codes).columns(pk.Column("code", SqlTypes.INTEGER)).get_sql()
|
|
62
|
+
)
|
|
63
|
+
for batch in batched(lookup.keys(), 1000):
|
|
64
|
+
placeholders = ",".join(map(lambda x: f"('{x}')", batch))
|
|
65
|
+
cursor.execute(f"insert into #rkd_codes values {placeholders};")
|
|
66
|
+
|
|
67
|
+
cursor.execute(query.get_sql())
|
|
68
|
+
|
|
69
|
+
for row in dictfetchall(cursor, OfficerDataDict):
|
|
58
70
|
row["instrument_id"] = lookup[row["external_identifier"]]
|
|
59
71
|
yield row
|
|
72
|
+
|
|
73
|
+
# Clean up temporary table
|
|
74
|
+
cursor.execute(pk.MSSQLQuery.drop_table(temp_codes).get_sql())
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from pypika.terms import ValueWrapper
|
|
4
|
+
from pypika import Column, MSSQLQuery, Table
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from pypika.terms import Term
|
|
8
|
+
from pypika.queries import CreateQueryBuilder
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def compile_source(source: str) -> "Term":
|
|
12
|
+
return ValueWrapper(source).as_("source")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SOURCE_DS2 = compile_source("qa-ds2")
|
|
16
|
+
SOURCE_RKD = compile_source("qa-rkd")
|
|
17
|
+
SOURCE_IBES = compile_source("qa-ibes")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_table(tablename: "str", *columns: "Column") -> tuple[Table, "CreateQueryBuilder"]:
|
|
21
|
+
table = Table(tablename)
|
|
22
|
+
return table, MSSQLQuery.create_table(table).columns(*columns)
|
wbfdm/dataloaders/protocols.py
CHANGED
|
@@ -27,13 +27,13 @@ from wbfdm.enums import (
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class ReportDateProtocol(Protocol):
|
|
30
|
-
def reporting_dates(self, only_next: bool = True) -> Iterator[ReportDateDataDict]:
|
|
31
|
-
...
|
|
30
|
+
def reporting_dates(self, only_next: bool = True) -> Iterator[ReportDateDataDict]: ...
|
|
32
31
|
|
|
33
32
|
|
|
34
33
|
class AdjustmentsProtocol(Protocol):
|
|
35
|
-
def adjustments(
|
|
36
|
-
|
|
34
|
+
def adjustments(
|
|
35
|
+
self, from_date: date | None = None, to_date: date | None = None
|
|
36
|
+
) -> Iterator[AdjustmentDataDict]: ...
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
class MarketDataProtocol(Protocol):
|
|
@@ -45,20 +45,17 @@ class MarketDataProtocol(Protocol):
|
|
|
45
45
|
exact_date: date | None = None,
|
|
46
46
|
frequency: Frequency = Frequency.DAILY,
|
|
47
47
|
target_currency: str | None = None,
|
|
48
|
-
) -> Iterator[MarketDataDict]:
|
|
49
|
-
...
|
|
48
|
+
) -> Iterator[MarketDataDict]: ...
|
|
50
49
|
|
|
51
50
|
|
|
52
51
|
class CorporateActionsProtocol(Protocol):
|
|
53
52
|
def corporate_actions(
|
|
54
53
|
self, from_date: date | None = None, to_date: date | None = None
|
|
55
|
-
) -> Iterator[CorporateActionDataDict]:
|
|
56
|
-
...
|
|
54
|
+
) -> Iterator[CorporateActionDataDict]: ...
|
|
57
55
|
|
|
58
56
|
|
|
59
57
|
class OfficersProtocol(Protocol):
|
|
60
|
-
def officers(self) -> Iterator[OfficerDataDict]:
|
|
61
|
-
...
|
|
58
|
+
def officers(self) -> Iterator[OfficerDataDict]: ...
|
|
62
59
|
|
|
63
60
|
|
|
64
61
|
class StatementsProtocol(Protocol):
|
|
@@ -73,8 +70,7 @@ class StatementsProtocol(Protocol):
|
|
|
73
70
|
data_type: DataType = DataType.STANDARDIZED,
|
|
74
71
|
financials: list[Financial] | None = None,
|
|
75
72
|
target_currency: str | None = None,
|
|
76
|
-
) -> Iterator[StatementDataDict]:
|
|
77
|
-
...
|
|
73
|
+
) -> Iterator[StatementDataDict]: ...
|
|
78
74
|
|
|
79
75
|
|
|
80
76
|
class FinancialsProtocol(Protocol):
|
|
@@ -95,18 +91,15 @@ class FinancialsProtocol(Protocol):
|
|
|
95
91
|
data_type: DataType = DataType.STANDARDIZED,
|
|
96
92
|
estimate_type: EstimateType = EstimateType.VALID,
|
|
97
93
|
target_currency: str | None = None,
|
|
98
|
-
) -> Iterator[FinancialDataDict]:
|
|
99
|
-
...
|
|
94
|
+
) -> Iterator[FinancialDataDict]: ...
|
|
100
95
|
|
|
101
96
|
|
|
102
97
|
class ESGControversyProtocol(Protocol):
|
|
103
|
-
def esg_controversies(self) -> Iterator[ESGControversyDataDict]:
|
|
104
|
-
...
|
|
98
|
+
def esg_controversies(self) -> Iterator[ESGControversyDataDict]: ...
|
|
105
99
|
|
|
106
100
|
|
|
107
101
|
class ESGProtocol(Protocol):
|
|
108
102
|
def esg(
|
|
109
103
|
self,
|
|
110
104
|
values: list[ESG],
|
|
111
|
-
) -> Iterator[ESGDataDict]:
|
|
112
|
-
...
|
|
105
|
+
) -> Iterator[ESGDataDict]: ...
|
wbfdm/dataloaders/proxies.py
CHANGED
|
@@ -67,7 +67,7 @@ class InstrumentDataloaderProxy(
|
|
|
67
67
|
for dl in self.iterate_dataloaders("reporting_dates"):
|
|
68
68
|
yield from dl.reporting_dates(only_next=only_next)
|
|
69
69
|
|
|
70
|
-
def adjustments(self, from_date: date, to_date: date) -> Iterator[AdjustmentDataDict]:
|
|
70
|
+
def adjustments(self, from_date: date | None = None, to_date: date | None = None) -> Iterator[AdjustmentDataDict]:
|
|
71
71
|
for dl in self.iterate_dataloaders("adjustments"):
|
|
72
72
|
yield from dl.adjustments(from_date=from_date, to_date=to_date)
|
|
73
73
|
|
wbfdm/dataloaders/types.py
CHANGED
|
@@ -33,6 +33,8 @@ class MarketDataDict(BaseDict):
|
|
|
33
33
|
Attributes:
|
|
34
34
|
valuation_date: date
|
|
35
35
|
The date of valuation.
|
|
36
|
+
external_identifier: str
|
|
37
|
+
The external identifier of the instrument
|
|
36
38
|
open: float | None
|
|
37
39
|
The opening value (if available).
|
|
38
40
|
close: float | None
|
|
@@ -52,6 +54,7 @@ class MarketDataDict(BaseDict):
|
|
|
52
54
|
"""
|
|
53
55
|
|
|
54
56
|
valuation_date: date
|
|
57
|
+
external_identifier: str
|
|
55
58
|
|
|
56
59
|
open: NotRequired[float]
|
|
57
60
|
close: NotRequired[float]
|
wbfdm/enums.py
CHANGED
|
@@ -50,12 +50,12 @@ class Financial(ChoiceEnum):
|
|
|
50
50
|
NET_INCOME_BEFORE_TAXES = "pbt"
|
|
51
51
|
NET_INCOME = "net_income"
|
|
52
52
|
EPS = "eps"
|
|
53
|
-
FREE_CASH_FLOW = "
|
|
53
|
+
FREE_CASH_FLOW = "free_cash_flow"
|
|
54
54
|
EBITDA = "ebitda"
|
|
55
55
|
EBITDA_PER_SHARE = "ebitda_sh"
|
|
56
56
|
NET_DEBT = "net_debt"
|
|
57
57
|
ENTERPRISE_VALUE = "ev"
|
|
58
|
-
SHARES_OUTSTANDING = "
|
|
58
|
+
SHARES_OUTSTANDING = "shares_outstanding"
|
|
59
59
|
COST_OF_GOODS_SOLD = "cogs"
|
|
60
60
|
GROSS_PROFIT_MARGIN = "gross_profit_margin"
|
|
61
61
|
SELLING_MARKETING_EXPENSES = "selling_marketing_expenses"
|
|
@@ -79,7 +79,7 @@ class Financial(ChoiceEnum):
|
|
|
79
79
|
CASH_FLOW_FROM_OPERATIONS = "cash_flow_from_operations"
|
|
80
80
|
CAPEX = "capex"
|
|
81
81
|
CASH_FLOW_FROM_INVESTING = "cash_flow_from_investing"
|
|
82
|
-
FREE_CASH_FLOW_PER_SHARE = "
|
|
82
|
+
FREE_CASH_FLOW_PER_SHARE = "free_cash_flow_per_share"
|
|
83
83
|
TOTAL_DIVIDENDS = "total_dividends"
|
|
84
84
|
CASH_FLOW_FROM_FINANCING = "cash_flow_from_financing"
|
|
85
85
|
CASH_FLOW_PER_SHARE = "cash_flow_per_share"
|