wbfdm 1.43.1__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.

@@ -136,7 +136,7 @@ class InstrumentPMSMixin:
136
136
  self, start: Optional[date] = None, end: Optional[date] = None, **kwargs
137
137
  ) -> pd.DataFrame:
138
138
  if not (prices := self.get_prices_df_with_calculated(from_date=start, to_date=end, **kwargs)).empty:
139
- calculated_mask = prices[["calculated"]].copy().asfreq("BME", method="ffill")
139
+ calculated_mask = prices[["calculated"]].copy().groupby([prices.index.year, prices.index.month]).tail(1)
140
140
  calculated_mask["year"] = calculated_mask.index.year
141
141
  calculated_mask["month"] = calculated_mask.index.month
142
142
  calculated_mask = (
@@ -192,6 +192,7 @@ class InstrumentModelSerializer(InstrumentAdditionalResourcesMixin, InstrumentMo
192
192
  if not instance.is_managed:
193
193
  res.update(
194
194
  {
195
+ "fin-summary": reverse("wbfdm:financial-summary-list", args=[instance.id], request=request),
195
196
  "swe-income-statement": reverse(
196
197
  "wbfdm:statementwithestimates-list", args=[instance.id, "income"], request=request
197
198
  ),
@@ -380,7 +380,7 @@ class TestStatementWithEstimates:
380
380
  "employees",
381
381
  "stock_compensation_employee_ratio",
382
382
  "capex",
383
- "outstanding_shares",
383
+ "shares_outstanding",
384
384
  "market_capitalization",
385
385
  "close",
386
386
  "market_capitalization",
@@ -88,7 +88,7 @@ class TestInstrumentModel:
88
88
 
89
89
  def test_get_monthly_return_summary(self, instrument, instrument_price_factory):
90
90
  instrument_price_factory.create_batch(10, instrument=instrument)
91
- res, _ = instrument.get_monthly_return_summary()
91
+ res, calculated_mask = instrument.get_monthly_return_summary()
92
92
  assert "performance" in res.columns
93
93
  assert "month" in res.columns
94
94
  assert "year" in res.columns
@@ -100,6 +100,8 @@ class TestInstrumentModel:
100
100
  for month in res[year].keys():
101
101
  assert month in calendar.month_abbr or month == "annual"
102
102
  assert "performance" in res[year][month]
103
+ if month == "annual":
104
+ assert res[year][month]["performance"] is not None
103
105
 
104
106
  def test_get_prices_df(self, instrument, instrument_price_factory):
105
107
  price = instrument_price_factory.create(instrument=instrument)
wbfdm/urls.py CHANGED
@@ -195,6 +195,7 @@ instrument_router.register(
195
195
  )
196
196
  instrument_router.register(r"valuation_ratios", viewsets.ValuationRatioChartViewSet, basename="valuation_ratios")
197
197
  instrument_router.register(r"prices", viewsets.InstrumentPriceViewSet, basename="prices")
198
+ instrument_router.register(r"financial-summary", viewsets.FinancialSummary, basename="financial-summary")
198
199
 
199
200
  instrument_statement_router = WBCoreRouter()
200
201
  instrument_statement_router.register(
@@ -232,7 +232,7 @@ class InstrumentButtonViewConfig(ButtonViewConfig):
232
232
  key="pai",
233
233
  ),
234
234
  ],
235
- )
235
+ ),
236
236
  # bt.DropDownButton(
237
237
  # label="Financial Analysis (Old)",
238
238
  # weight=3,
@@ -20,3 +20,4 @@ from .instruments_relationships import (
20
20
  from .exchanges import ExchangeDisplayConfig
21
21
  from .monthly_performances import MonthlyPerformancesInstrumentDisplayViewConfig
22
22
  from .esg import InstrumentESGPAIDisplayViewConfig, InstrumentESGControversyDisplayViewConfig
23
+ from .financial_summary import FinancialSummaryDisplayViewConfig
@@ -0,0 +1,133 @@
1
+ import typing
2
+
3
+ from wbcore.contrib.color.enums import WBColor
4
+ from wbcore.metadata.configs import display as dp
5
+ from wbcore.metadata.configs.display import DisplayViewConfig
6
+
7
+ if typing.TYPE_CHECKING:
8
+ from wbfdm.viewsets import FinancialSummary
9
+
10
+
11
+ class FinancialSummaryDisplayViewConfig(DisplayViewConfig):
12
+ view: "FinancialSummary"
13
+
14
+ ESTIMATE_COLOR = "#D2E5F6"
15
+
16
+ def get_list_display(self) -> dp.ListDisplay:
17
+ def generate_formatting_rules(col_key: str) -> typing.Iterator[dp.FormattingRule]:
18
+ yield dp.FormattingRule(
19
+ condition=[("==", "eps_growth", "id")],
20
+ style={
21
+ "borderBottom": "1px solid #000000",
22
+ },
23
+ )
24
+ yield dp.FormattingRule(
25
+ condition=[("==", "roic", "id")],
26
+ style={
27
+ "borderBottom": "1px solid #000000",
28
+ },
29
+ )
30
+ yield dp.FormattingRule(
31
+ condition=[("==", "interest_coverage_ratio", "id")],
32
+ style={
33
+ "borderBottom": "1px solid #000000",
34
+ },
35
+ )
36
+ yield dp.FormattingRule(
37
+ condition=[("==", "revenue_growth", "id")],
38
+ style={
39
+ "color": "#9FA0A1",
40
+ "fontStyle": "italic",
41
+ },
42
+ )
43
+ yield dp.FormattingRule(
44
+ condition=[("==", "net_profit_growth", "id")],
45
+ style={
46
+ "color": "#9FA0A1",
47
+ "fontStyle": "italic",
48
+ },
49
+ )
50
+ yield dp.FormattingRule(
51
+ condition=[("==", "eps_growth", "id")],
52
+ style={
53
+ "color": "#9FA0A1",
54
+ "fontStyle": "italic",
55
+ },
56
+ )
57
+ yield dp.FormattingRule(
58
+ condition=[("==", "free_cash_flow_per_share_growth", "id")],
59
+ style={
60
+ "color": "#9FA0A1",
61
+ "fontStyle": "italic",
62
+ },
63
+ )
64
+ yield dp.FormattingRule(
65
+ condition=[("==", "year", "id")],
66
+ style={
67
+ "fontWeight": "bold",
68
+ },
69
+ )
70
+ yield dp.FormattingRule(
71
+ style={"color": WBColor.RED_DARK.value},
72
+ condition=[("<", 0)],
73
+ )
74
+ if self.view.estimate_columns.get(col_key, False) is True:
75
+ yield dp.FormattingRule(style={"background-color": self.ESTIMATE_COLOR})
76
+
77
+ def generate_field(col: str) -> dp.Field:
78
+ return dp.Field(
79
+ key=col,
80
+ label=col,
81
+ width=80,
82
+ formatting_rules=generate_formatting_rules(col),
83
+ auto_size=False,
84
+ resizable=False,
85
+ movable=False,
86
+ menu=False,
87
+ size_to_fit=False,
88
+ )
89
+
90
+ return dp.ListDisplay(
91
+ fields=[
92
+ dp.Field(
93
+ key="label",
94
+ label=" ",
95
+ width=120,
96
+ auto_size=False,
97
+ resizable=False,
98
+ movable=False,
99
+ menu=False,
100
+ size_to_fit=False,
101
+ formatting_rules=[
102
+ dp.FormattingRule(
103
+ condition=[("==", "eps_growth", "id")],
104
+ style={
105
+ "borderBottom": "1px solid #000000",
106
+ },
107
+ ),
108
+ dp.FormattingRule(
109
+ condition=[("==", "roic", "id")],
110
+ style={
111
+ "borderBottom": "1px solid #000000",
112
+ },
113
+ ),
114
+ dp.FormattingRule(
115
+ condition=[("==", "interest_coverage_ratio", "id")],
116
+ style={
117
+ "borderBottom": "1px solid #000000",
118
+ },
119
+ ),
120
+ dp.FormattingRule(style={"background-color": "#ECECEC"}),
121
+ dp.FormattingRule(
122
+ condition=[("==", "year", "id")],
123
+ style={
124
+ "fontWeight": "bold",
125
+ },
126
+ ),
127
+ ],
128
+ ),
129
+ *map(generate_field, self.view.fiscal_columns),
130
+ ],
131
+ condensed=True,
132
+ editable=False,
133
+ )
@@ -17,7 +17,7 @@ from wbcore.metadata.configs.display.instance_display.shortcuts import (
17
17
  default,
18
18
  )
19
19
  from wbcore.metadata.configs.display.instance_display.styles import Style
20
- from wbcore.metadata.configs.display.instance_display.utils import repeat
20
+ from wbcore.metadata.configs.display.instance_display.utils import repeat, repeat_field
21
21
  from wbfdm.contrib.metric.viewsets.configs.utils import (
22
22
  get_performance_fields,
23
23
  get_statistic_field,
@@ -123,19 +123,18 @@ class InstrumentDisplayConfig(DisplayViewConfig):
123
123
  layouts={
124
124
  default(): Layout(
125
125
  grid_template_areas=[
126
- ["name", "name_repr", "market_data", "valuation_ratios-new"],
127
- ["instrument_type", "exchange", "market_data", "valuation_ratios-new"],
128
- ["isin", "ticker", "market_data", "valuation_ratios-new"],
129
- ["currency", "country", "market_data", "valuation_ratios-new"],
130
- ["inception_date", "delisted_date", "market_data", "valuation_ratios-new"],
131
- [".", ".", "market_data", "valuation_ratios-new"],
132
- ["description", "description", "description", "description"],
126
+ ["name", "ticker", "isin", "exchange", "market_data_chart", "valuation_ratios-new"],
127
+ ["name_repr", "instrument_type", "currency", "country", "market_data_chart", "valuation_ratios-new"],
128
+ ["inception_date", "delisted_date", ".", ".", "market_data_chart", "valuation_ratios-new"],
129
+ ["description", "description", "description", "description", "market_data_chart", "valuation_ratios-new"],
130
+ [repeat_field(4, "fin-summary"), repeat_field(2, ".")],
133
131
  ],
134
- grid_template_rows=["auto"] * 6 + ["300px"],
135
- grid_template_columns=["1fr", "1fr", "500px", "500px"],
132
+ grid_template_rows=["min-content"] * 3 + ["1fr", "446px"],
133
+ grid_template_columns=[repeat(4, "183px"), "1fr", "1fr"],
136
134
  inlines=[
137
- Inline(key="market_data", endpoint="market_data"),
138
- Inline(key="valuation_ratios-new", endpoint="valuation_ratios-new"),
135
+ Inline(key="market_data_chart", endpoint="market_data", title="Prices"),
136
+ Inline(key="valuation_ratios-new", endpoint="valuation_ratios-new", title="Ratios"),
137
+ Inline(key="fin-summary", endpoint="fin-summary", title="Financial Summary", hide_controls=True),
139
138
  ],
140
139
  ),
141
140
  },
@@ -1,3 +1,4 @@
1
1
  from .statement_with_estimates import StatementWithEstimatesPandasViewSet
2
2
  from .financial_metric_analysis import FinancialMetricAnalysisPandasViewSet
3
3
  from .financial_ratio_analysis import ValuationRatioChartViewSet
4
+ from .financial_summary import FinancialSummary
@@ -0,0 +1,256 @@
1
+ import pandas as pd
2
+ from django.utils.functional import cached_property
3
+ from wbcore.contrib.io.viewsets import ExportPandasAPIViewSet
4
+ from wbcore.metadata.configs.endpoints import NoEndpointViewConfig
5
+ from wbcore.pandas import fields as pf
6
+ from wbcore.pandas.utils import sanitize_fields, pct_change_with_negative_values
7
+ from wbcore.pandas.utils import (
8
+ override_number_to_decimal,
9
+ override_number_to_percent,
10
+ override_number_to_x,
11
+ override_number_to_integer_without_decorations,
12
+ )
13
+ from wbfdm.enums import Financial
14
+ from wbfdm.models.instruments import Instrument
15
+
16
+ from wbfdm.viewsets.configs.display import (
17
+ FinancialSummaryDisplayViewConfig,
18
+ )
19
+
20
+
21
+ from ..mixins import InstrumentMixin
22
+
23
+
24
+ class FinancialSummary(InstrumentMixin, ExportPandasAPIViewSet):
25
+ queryset = Instrument.objects.none()
26
+ display_config_class = FinancialSummaryDisplayViewConfig
27
+ ordering_fields = "__all__"
28
+ endpoint_config_class = NoEndpointViewConfig
29
+
30
+ def get_queryset(self):
31
+ return Instrument.objects.filter(id=self.instrument.id)
32
+
33
+ def get_pandas_fields(self, request):
34
+ return pf.PandasFields(
35
+ fields=[
36
+ pf.PKField(key="id", label="ID"),
37
+ pf.CharField(key="label", label="Financial"),
38
+ pf.JsonField(key="_overwrites", label="Overwrites"),
39
+ *[pf.FloatField(key=k, label=k, precision=1) for k in self.fiscal_columns],
40
+ ]
41
+ )
42
+
43
+ def get_dataframe(self, request, queryset, **kwargs):
44
+ # Get all necessary data from the dataloader and load a dataframe
45
+ df = pd.DataFrame(
46
+ queryset.dl.financials(
47
+ values=self.FINANCIAL_VALUES,
48
+ from_index=-5,
49
+ to_index=3,
50
+ )
51
+ )
52
+
53
+ # Pivot the data
54
+ df = df.pivot_table(
55
+ columns="financial",
56
+ index=["year", "period_end_date", "estimate"],
57
+ values="value",
58
+ ).rename_axis(columns=None)
59
+
60
+ sanitize_fields(df, map(lambda enum: enum.value, self.FINANCIAL_VALUES))
61
+
62
+ # Compute all necessary fields
63
+ df["revenue_growth"] = df["revenue"].pct_change() * 100
64
+ df["gross_profit_pct"] = df["gross_profit"] / df["revenue"] * 100
65
+ df["ebitda_pct"] = df["ebitda"] / df["revenue"] * 100
66
+ df["ebit_pct"] = df["ebit"] / df["revenue"] * 100
67
+ df["net_income_pct"] = df["net_income"] / df["revenue"] * 100
68
+ df["eps_growth"] = pct_change_with_negative_values(df, "eps") * 100
69
+
70
+ df["net_debt_ebitda"] = df["net_debt"] / df["ebitda"]
71
+ df["debt_assets"] = df["total_debt"] / df["total_assets"] * 100
72
+ df["debt_equity"] = df["total_debt"] / df["total_assets"] * 100
73
+
74
+ df["interest_coverage_ratio"] = df["ebit"] / df["interest_expense"]
75
+ df["free_cash_flow_per_share"] = df["free_cash_flow"] / df["shares_outstanding"]
76
+ df["free_cash_flow_per_share_growth"] = pct_change_with_negative_values(df, "free_cash_flow_per_share") * 100
77
+
78
+ # Normalize data
79
+ df["revenue"] = df["revenue"] / 1_000_000
80
+ df["net_income"] = df["net_income"] / 1_000_000
81
+
82
+ # Sort the columns into the desired order
83
+ # Reset the index to get the period end date as a column
84
+ # Pivot back to have the dates on top
85
+ df = df[self.FIELDS]
86
+
87
+ # Reset the 2 indices and transpose back
88
+ df = df.reset_index(level=[0, 2]).sort_index()
89
+
90
+ # Adjust the columns to be in a different format
91
+ df.index = df.index.map(lambda x: x.strftime("%b/%y"))
92
+ MAX_ROW = 8
93
+ if df.shape[0] > MAX_ROW:
94
+ df = df.iloc[1:] # remove first row
95
+ df = df.iloc[0 : min([df.shape[0], MAX_ROW])] # keep only 8 row maximum
96
+
97
+ self._estimate_columns = df["estimate"].to_dict()
98
+ df = df.drop(columns=["estimate"], errors="ignore")
99
+ return df
100
+
101
+ def manipulate_dataframe(self, df):
102
+ df = df.T
103
+ # Add labels for human readable output
104
+ df["label"] = self.LABELS
105
+
106
+ override_number_to_percent(
107
+ df,
108
+ *list(
109
+ map(
110
+ lambda x: df.index == x,
111
+ [
112
+ "revenue_growth",
113
+ "gross_profit_pct",
114
+ "ebitda_pct",
115
+ "ebit_pct",
116
+ "net_income_pct",
117
+ "eps_growth",
118
+ "roe",
119
+ "roic",
120
+ "roc",
121
+ "roa",
122
+ "debt_equity",
123
+ "debt_assets",
124
+ "free_cash_flow_per_share_growth",
125
+ ],
126
+ )
127
+ ),
128
+ )
129
+
130
+ override_number_to_x(
131
+ df,
132
+ *list(
133
+ map(
134
+ lambda x: df.index == x,
135
+ [
136
+ "net_debt_ebitda",
137
+ "interest_coverage_ratio",
138
+ ],
139
+ )
140
+ ),
141
+ )
142
+
143
+ override_number_to_decimal(
144
+ df,
145
+ *list(
146
+ map(
147
+ lambda x: df.index == x,
148
+ [
149
+ "revenue",
150
+ "net_income",
151
+ ],
152
+ )
153
+ ),
154
+ )
155
+
156
+ override_number_to_integer_without_decorations(
157
+ df,
158
+ *list(
159
+ map(
160
+ lambda x: df.index == x,
161
+ [
162
+ "year",
163
+ ],
164
+ )
165
+ ),
166
+ )
167
+
168
+ return df.reset_index(names="id")
169
+
170
+ @cached_property
171
+ def fiscal_columns(self) -> list:
172
+ """Returns the fiscal columns from the dataframe"""
173
+ return self.df.columns.difference(["label", "_overwrites", "id"]).to_list()
174
+
175
+ @cached_property
176
+ def estimate_columns(self) -> dict:
177
+ """Returns a dictionary with the estimate column for each fiscal column
178
+ The _estimate_columns will be set if the dataframe is constructed.
179
+ """
180
+ return getattr(self, "_estimate_columns", {})
181
+
182
+ @cached_property
183
+ def FINANCIAL_VALUES(self) -> list[Financial]:
184
+ return [
185
+ Financial.REVENUE, # SAL
186
+ Financial.GROSS_PROFIT, # GRI
187
+ Financial.EBITDA, # EBT
188
+ Financial.EBIT, # EBI
189
+ Financial.NET_INCOME, # NET
190
+ Financial.EPS, # EPS
191
+ Financial.SHAREHOLDERS_EQUITY, # SHE
192
+ Financial.TOTAL_ASSETS, # TAS
193
+ Financial.TAX_RATE, # TAX
194
+ Financial.RETURN_ON_INVESTED_CAPITAL, # RIC
195
+ Financial.NET_DEBT, # NDT
196
+ Financial.TOTAL_DEBT, # TDT
197
+ Financial.INTEREST_EXPENSE, # INE
198
+ Financial.FREE_CASH_FLOW, # FCF
199
+ Financial.SHARES_OUTSTANDING,
200
+ Financial.CURRENT_LIABILITIES, # CRL
201
+ Financial.CASH_EQUIVALENTS,
202
+ Financial.RETURN_ON_EQUITY,
203
+ Financial.RETURN_ON_ASSETS,
204
+ Financial.RETURN_ON_CAPITAL,
205
+ Financial.RETURN_ON_INVESTED_CAPITAL,
206
+ ]
207
+
208
+ @cached_property
209
+ def FIELDS(self) -> list[str]:
210
+ return [
211
+ "revenue",
212
+ "revenue_growth",
213
+ "gross_profit_pct",
214
+ "ebitda_pct",
215
+ "ebit_pct",
216
+ "net_income_pct",
217
+ "net_income",
218
+ "eps",
219
+ "eps_growth",
220
+ "roe",
221
+ "roa",
222
+ "roc",
223
+ "roic",
224
+ "net_debt_ebitda",
225
+ "debt_assets",
226
+ "debt_equity",
227
+ "interest_coverage_ratio",
228
+ "free_cash_flow_per_share",
229
+ "free_cash_flow_per_share_growth",
230
+ ]
231
+
232
+ @property
233
+ def LABELS(self) -> list[str]:
234
+ currency_key = self.instrument.currency.key if self.instrument.currency else "N.A."
235
+ return [
236
+ f"in {currency_key} MN",
237
+ "Revenue",
238
+ "Y/Y Change",
239
+ "Gross Margin",
240
+ "EBITDA Margin",
241
+ "EBIT Margin",
242
+ "Net Profit Margin",
243
+ "Net Profit",
244
+ "EPS",
245
+ "Y/Y Change",
246
+ "ROE",
247
+ "ROA",
248
+ "ROC",
249
+ "ROIC",
250
+ "Net Debt/EBITDA",
251
+ "D/A",
252
+ "D/E",
253
+ "Int. Cov. Ratio",
254
+ "FCF per share",
255
+ "Y/Y Change",
256
+ ]
wbfdm/viewsets/mixins.py CHANGED
@@ -4,6 +4,8 @@ from wbfdm.models import Instrument
4
4
 
5
5
 
6
6
  class InstrumentMixin:
7
+ kwargs: dict
8
+
7
9
  @cached_property
8
10
  def instrument(self) -> Instrument:
9
11
  return get_object_or_404(Instrument, pk=self.kwargs["instrument_id"])
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbfdm
3
- Version: 1.43.1
3
+ Version: 1.44.0
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.*
7
+ Requires-Dist: sentry-sdk==2.*
7
8
  Requires-Dist: stockstats==0.6.*
8
9
  Requires-Dist: wbcore
9
10
  Requires-Dist: wbnews
@@ -13,3 +14,4 @@ Requires-Dist: requests-cache==1.0.*; extra == 'dsws'
13
14
  Provides-Extra: qa
14
15
  Requires-Dist: jinjasql2==0.1.*; extra == 'qa'
15
16
  Requires-Dist: mssql-django==1.4.*; extra == 'qa'
17
+ Requires-Dist: pypika; extra == 'qa'