fundamentalista 0.1.0 → 0.2.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +29 -2
- data/lib/fundamentalista/balance_sheet.rb +7 -4
- data/lib/fundamentalista/company.rb +34 -1
- data/lib/fundamentalista/income_statement.rb +10 -3
- data/lib/fundamentalista/price_history.rb +90 -0
- data/lib/fundamentalista/provider.rb +7 -2
- data/lib/fundamentalista/providers/edgar/tags.rb +9 -7
- data/lib/fundamentalista/providers/edgar.rb +4 -0
- data/lib/fundamentalista/providers/fmp.rb +12 -5
- data/lib/fundamentalista/ratios.rb +5 -5
- data/lib/fundamentalista/scores/altman_z.rb +1 -1
- data/lib/fundamentalista/valuation.rb +33 -5
- data/lib/fundamentalista/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 36260d14ca8a99975cc3c9717ecd1fa5694f2e2d29144aaeb8d51e9eed3a25a5
|
|
4
|
+
data.tar.gz: 920282a4dd650e6ed172638e02b85c37b3ac6e0166af5f7a837b4e69003d1c59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 698002a96de44e7c842e619fbce287758f2478f56f0e0a3c1fcbb131a635e88316b9609bfba710c7223627f6ea7ba67b5a3ad5cc5a9296cf34b061a65b76956f
|
|
7
|
+
data.tar.gz: 675e708b8b952f95aa50ef374165b37fa70f9d7d6e217ee8b14fb7a8157fec4c1402f8a0703aba8d4808fa33e0202a75eeafc43e2fd76a8bdba358b63f044763
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- Daily prices from FMP as a `PriceHistory` with dividend adjusted returns, the valuation of every fiscal year at its closing price, and the company beta.
|
|
6
|
+
- The weighted average cost of capital as a discount rate built from the capital structure.
|
|
7
|
+
|
|
8
|
+
## 0.1.1
|
|
9
|
+
|
|
10
|
+
- EBIT derived from income before tax and interest when operating income is not reported, used by interest coverage, EV/EBIT, the magic formula and the Altman Z-score.
|
|
11
|
+
- Minority interest on the balance sheet, so derived total liabilities match the reported ones.
|
|
12
|
+
- More concepts checked against Financial Modeling Prep figures: current maturities of long term debt, finance lease interest, other short term investments.
|
|
13
|
+
|
|
3
14
|
## 0.1.0
|
|
4
15
|
|
|
5
16
|
- Financial statements as value objects with BigDecimal figures and derived line items.
|
data/README.md
CHANGED
|
@@ -90,8 +90,10 @@ EDGAR reports quarterly cash flows only year to date, and the fourth quarter onl
|
|
|
90
90
|
|
|
91
91
|
| Statement | Reported items | Derived when missing |
|
|
92
92
|
| --- | --- | --- |
|
|
93
|
-
| `IncomeStatement` | revenue, cost of revenue, gross profit, SG&A, operating income, interest expense, income before tax, income tax, net income, depreciation and amortization, EBITDA, diluted EPS, diluted shares | gross profit, EBITDA, diluted EPS, tax rate |
|
|
94
|
-
| `BalanceSheet` | cash, short term investments, receivables, inventory, current assets, PP&E, total assets, payables, current liabilities, total liabilities, short and long term debt, total debt, equity, retained earnings, shares outstanding | total liabilities, total debt, liquid assets, net debt, working capital, net working capital |
|
|
93
|
+
| `IncomeStatement` | revenue, cost of revenue, gross profit, SG&A, operating income, EBIT, interest expense, income before tax, income tax, net income, depreciation and amortization, EBITDA, diluted EPS, diluted shares | gross profit, EBIT, EBITDA, diluted EPS, tax rate |
|
|
94
|
+
| `BalanceSheet` | cash, short term investments, receivables, inventory, current assets, PP&E, total assets, payables, current liabilities, total liabilities, short and long term debt, total debt, equity, minority interest, retained earnings, shares outstanding | total liabilities, total debt, liquid assets, net debt, working capital, net working capital |
|
|
95
|
+
|
|
96
|
+
EBIT is operating income when the company reports one, and income before tax plus interest expense otherwise, which is what pharmaceutical and oil companies leave you; interest coverage, EV/EBIT, Greenblatt's yield and return on capital and the Altman Z-score all use it. Figures follow the line as reported: receivables are trade receivables, PP&E excludes lease right-of-use assets, cost of revenue is the company's own line.
|
|
95
97
|
| `CashFlowStatement` | operating cash flow, capital expenditure, free cash flow, dividends paid, share repurchases | free cash flow, shareholder returns |
|
|
96
98
|
|
|
97
99
|
Outflows are positive amounts: `capital_expenditure` is what was spent.
|
|
@@ -220,6 +222,15 @@ estimate.analysts # how many stand behind it
|
|
|
220
222
|
estimate.eps_growth(year.income.eps_diluted)
|
|
221
223
|
```
|
|
222
224
|
|
|
225
|
+
### The discount rate
|
|
226
|
+
|
|
227
|
+
`wacc` builds a discount rate from the capital structure at the current price: the cost of equity by CAPM from `beta`, the risk free rate and the equity premium, and the after tax cost of debt from interest expense over total debt, weighted by market value. FMP profiles carry `company.beta`; on EDGAR pass your own.
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
rate = valuation.wacc(beta: company.beta, risk_free: 0.04, equity_premium: 0.05)
|
|
231
|
+
valuation.intrinsic_value(growth: 0.06, discount_rate: rate)
|
|
232
|
+
```
|
|
233
|
+
|
|
223
234
|
### Discounted cash flow
|
|
224
235
|
|
|
225
236
|
`intrinsic_value` grows the period's free cash flow at `growth` for `years`, adds a terminal value at `terminal_growth`, discounts everything at `discount_rate`, subtracts net debt, and divides by the shares outstanding. `margin_of_safety` is how far the price sits below that value.
|
|
@@ -244,6 +255,21 @@ dcf.terminal_value
|
|
|
244
255
|
dcf.value
|
|
245
256
|
```
|
|
246
257
|
|
|
258
|
+
## Prices and history
|
|
259
|
+
|
|
260
|
+
FMP serves daily closes with their dividend adjusted counterparts as a `PriceHistory`; on EDGAR you build one yourself from any source. With prices, every fiscal year can be valued at the close of its last day, which is what a history of multiples is.
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
prices = company.prices(from: Date.new(2020, 1, 1)) # FMP
|
|
264
|
+
prices = Fundamentalista::PriceHistory.new([[Date.new(2025, 9, 26), 254.52, 254.52], ...])
|
|
265
|
+
prices.at(Date.new(2025, 9, 27)).close # the last close on or before that day
|
|
266
|
+
prices.total_return(from: Date.new(2020, 1, 1)) # dividends reinvested
|
|
267
|
+
prices.annualized_return
|
|
268
|
+
|
|
269
|
+
company.valuation_history(:pe, :pb, :fcf_yield) # {2025 => {price:, pe:, pb:, fcf_yield:}, ...}
|
|
270
|
+
company.valuation_history(:pe, prices: prices) # with a history you bring
|
|
271
|
+
```
|
|
272
|
+
|
|
247
273
|
## Providers
|
|
248
274
|
|
|
249
275
|
| | EDGAR | Financial Modeling Prep |
|
|
@@ -252,6 +278,7 @@ dcf.value
|
|
|
252
278
|
| Periods | Annual from 10-K filings, quarterly from 10-Q filings | Annual and quarterly |
|
|
253
279
|
| Quotes | No, pass a price | Yes |
|
|
254
280
|
| Analyst estimates | No, pass an EPS | Yes |
|
|
281
|
+
| Prices and beta | No, bring a PriceHistory and a beta | Yes |
|
|
255
282
|
| Coverage | Companies filing with the SEC, US GAAP and IFRS, in their reporting currency | Global |
|
|
256
283
|
|
|
257
284
|
EDGAR publishes every value a company ever tagged, restatements included. Fundamentalista reads each line item for the period it describes and keeps the most recently filed value, while the fiscal year label comes from the original filing. Companies tag the same idea under different XBRL concepts, so each line item has an ordered list of concepts in `Providers::Edgar::Tags`, and the first one reported wins. Debt excludes lease obligations, which FMP's `totalDebt` includes.
|
|
@@ -3,15 +3,18 @@
|
|
|
3
3
|
module Fundamentalista
|
|
4
4
|
# The balance sheet at the end of a period. +shares_outstanding+ is a
|
|
5
5
|
# share count, +ppe+ is property, plant and equipment net of
|
|
6
|
-
# depreciation
|
|
6
|
+
# depreciation, +equity+ belongs to the parent's shareholders and
|
|
7
|
+
# +minority_interest+ to others; everything else is in the reporting
|
|
8
|
+
# currency.
|
|
7
9
|
class BalanceSheet < Statement
|
|
8
10
|
field :cash, :short_term_investments, :receivables, :inventory, :current_assets, :ppe, :total_assets,
|
|
9
11
|
:payables, :current_liabilities, :total_liabilities, :short_term_debt, :long_term_debt, :total_debt,
|
|
10
|
-
:equity, :retained_earnings, :shares_outstanding
|
|
12
|
+
:equity, :minority_interest, :retained_earnings, :shares_outstanding
|
|
11
13
|
|
|
12
|
-
# Total liabilities, derived from total assets
|
|
14
|
+
# Total liabilities, derived from total assets, equity and minority
|
|
15
|
+
# interest when not reported.
|
|
13
16
|
def total_liabilities
|
|
14
|
-
@total_liabilities || Decimal.subtract(total_assets, equity)
|
|
17
|
+
@total_liabilities || Decimal.subtract(total_assets, Decimal.sum(equity, minority_interest || BigDecimal('0')))
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
# Total debt, derived from short and long term debt when not reported.
|
|
@@ -11,19 +11,25 @@ module Fundamentalista
|
|
|
11
11
|
# company.ttm.ratios.net_margin
|
|
12
12
|
# company.valuation(price: 320).pe
|
|
13
13
|
# company.estimate.eps # FMP only
|
|
14
|
+
# company.valuation_history(:pe) # {2025 => {price:, pe:}, ...}
|
|
14
15
|
#
|
|
15
16
|
class Company
|
|
16
17
|
include Inspectable
|
|
17
18
|
|
|
18
19
|
attr_reader :ticker, :name, :cik, :currency, :provider
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
# The stock's beta, when the provider reports one.
|
|
22
|
+
attr_reader :beta
|
|
23
|
+
|
|
24
|
+
def initialize(ticker:, name:, provider:, cik: nil, currency: 'USD', beta: nil)
|
|
21
25
|
@ticker = ticker
|
|
22
26
|
@name = name
|
|
23
27
|
@cik = cik
|
|
24
28
|
@currency = currency
|
|
29
|
+
@beta = Decimal.wrap(beta)
|
|
25
30
|
@provider = provider
|
|
26
31
|
@financials = {}
|
|
32
|
+
@prices = {}
|
|
27
33
|
end
|
|
28
34
|
|
|
29
35
|
# Returns the Financials, newest first. +period+ is +:annual+ or
|
|
@@ -32,6 +38,33 @@ module Fundamentalista
|
|
|
32
38
|
@financials[[period, limit]] ||= provider.financials(self, period: period, limit: limit)
|
|
33
39
|
end
|
|
34
40
|
|
|
41
|
+
# Returns the PriceHistory between +from+ and +to+. Raises
|
|
42
|
+
# QuoteUnavailableError when the provider has no prices, as EDGAR does
|
|
43
|
+
# not; build a PriceHistory yourself there.
|
|
44
|
+
def prices(from:, to: Date.today)
|
|
45
|
+
@prices[[from, to]] ||= provider.prices(self, from: from, to: to)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns +metrics+ of the Valuation of every annual period at the
|
|
49
|
+
# close of its last day, keyed by fiscal year, newest first. Takes the
|
|
50
|
+
# provider's +prices+ unless a PriceHistory is given. Years without a
|
|
51
|
+
# price are left out.
|
|
52
|
+
#
|
|
53
|
+
# company.valuation_history(:pe, :fcf_yield)
|
|
54
|
+
# # => {2025 => {price: 0.25452e3, pe: 0.3412e2, fcf_yield: 0.026e-1}, ...}
|
|
55
|
+
#
|
|
56
|
+
def valuation_history(*metrics, prices: nil)
|
|
57
|
+
periods = financials.to_a
|
|
58
|
+
prices ||= self.prices(from: periods.last.ended_on)
|
|
59
|
+
periods.filter_map do |period|
|
|
60
|
+
price = prices.at(period.ended_on)
|
|
61
|
+
next unless price
|
|
62
|
+
|
|
63
|
+
valuation = Valuation.new(period, Quote.new(price: price.close))
|
|
64
|
+
[period.fiscal_year, { price: price.close, **metrics.to_h { |metric| [metric, valuation.public_send(metric)] } }]
|
|
65
|
+
end.to_h
|
|
66
|
+
end
|
|
67
|
+
|
|
35
68
|
# Returns the trailing twelve months as a Period, built from the last
|
|
36
69
|
# +quarters+ quarterly periods; eight give it a prior period too.
|
|
37
70
|
def ttm(quarters: 8)
|
|
@@ -5,7 +5,7 @@ module Fundamentalista
|
|
|
5
5
|
# currency; +eps_diluted+ is per share, +diluted_shares+ a share count
|
|
6
6
|
# and +sga+ the selling, general and administrative expense.
|
|
7
7
|
class IncomeStatement < Statement
|
|
8
|
-
field :revenue, :cost_of_revenue, :gross_profit, :sga, :operating_income, :interest_expense,
|
|
8
|
+
field :revenue, :cost_of_revenue, :gross_profit, :sga, :operating_income, :ebit, :interest_expense,
|
|
9
9
|
:income_before_tax, :income_tax, :net_income, :depreciation_amortization, :ebitda,
|
|
10
10
|
:eps_diluted, :diluted_shares
|
|
11
11
|
|
|
@@ -14,9 +14,16 @@ module Fundamentalista
|
|
|
14
14
|
@gross_profit || Decimal.subtract(revenue, cost_of_revenue)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
#
|
|
17
|
+
# Earnings before interest and taxes: operating income when reported,
|
|
18
|
+
# else income before tax plus interest expense, which is what
|
|
19
|
+
# companies without an operating income line leave you.
|
|
20
|
+
def ebit
|
|
21
|
+
@ebit || operating_income || Decimal.sum(income_before_tax, interest_expense)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# EBITDA, derived from EBIT and depreciation when not reported.
|
|
18
25
|
def ebitda
|
|
19
|
-
@ebitda || Decimal.sum(
|
|
26
|
+
@ebitda || Decimal.sum(ebit, depreciation_amortization)
|
|
20
27
|
end
|
|
21
28
|
|
|
22
29
|
# Diluted earnings per share, derived from net income and diluted shares
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fundamentalista
|
|
4
|
+
# Daily closes over a span of time, oldest first. Each Price carries the
|
|
5
|
+
# close and the dividend adjusted close, so multiples use the price of
|
|
6
|
+
# the day and returns include what was paid out.
|
|
7
|
+
#
|
|
8
|
+
# prices = company.prices(from: Date.new(2020, 1, 1)) # FMP
|
|
9
|
+
# prices = Fundamentalista::PriceHistory.new([[Date.new(2025, 9, 26), 254.52, 254.52], ...])
|
|
10
|
+
# prices.at(Date.new(2025, 9, 27)).close # the last close on or before that day
|
|
11
|
+
# prices.total_return(from: Date.new(2020, 1, 1)) # dividends reinvested
|
|
12
|
+
#
|
|
13
|
+
class PriceHistory
|
|
14
|
+
include Enumerable
|
|
15
|
+
include Inspectable
|
|
16
|
+
include Serializable
|
|
17
|
+
|
|
18
|
+
# One day's close and dividend adjusted close.
|
|
19
|
+
Price = Struct.new(:date, :close, :adjusted, keyword_init: true) do
|
|
20
|
+
include Serializable
|
|
21
|
+
|
|
22
|
+
def to_h
|
|
23
|
+
{ date: date, close: close, adjusted: adjusted }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(points)
|
|
28
|
+
@prices = points.map { |point| wrap(point) }.sort_by(&:date)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def each(&)
|
|
32
|
+
@prices.each(&)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def size
|
|
36
|
+
@prices.size
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def from
|
|
40
|
+
@prices.first&.date
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to
|
|
44
|
+
@prices.last&.date
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns the Price of the last trading day on or before +date+, or +nil+.
|
|
48
|
+
def at(date)
|
|
49
|
+
@prices.reverse_each.find { |price| price.date <= date }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns the return with dividends reinvested between +from+ and +to+,
|
|
53
|
+
# the whole history by default, as a rate, or +nil+ when either day
|
|
54
|
+
# has no price.
|
|
55
|
+
def total_return(from: self.from, to: self.to)
|
|
56
|
+
start = at(from)
|
|
57
|
+
finish = at(to)
|
|
58
|
+
return nil unless start && finish
|
|
59
|
+
|
|
60
|
+
Decimal.ratio(finish.adjusted, start.adjusted) - 1
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the total return between +from+ and +to+ annualized.
|
|
64
|
+
def annualized_return(from: self.from, to: self.to)
|
|
65
|
+
start = at(from)
|
|
66
|
+
finish = at(to)
|
|
67
|
+
return nil unless start && finish
|
|
68
|
+
|
|
69
|
+
Decimal.cagr(start.adjusted, finish.adjusted, (finish.date - start.date).to_f / 365.25)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def to_h
|
|
73
|
+
{ prices: @prices.map(&:to_h) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def inspect_attributes # :nodoc:
|
|
77
|
+
{ from: from, to: to, days: size }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def wrap(point)
|
|
83
|
+
return point if point.is_a?(Price)
|
|
84
|
+
|
|
85
|
+
date, close, adjusted = point
|
|
86
|
+
Price.new(date: date.is_a?(Date) ? date : Date.parse(date.to_s), close: Decimal.wrap(close),
|
|
87
|
+
adjusted: Decimal.wrap(adjusted || close))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Fundamentalista
|
|
4
4
|
# The contract every data source implements: resolve a Company, return
|
|
5
|
-
# its Financials, a Quote or +nil+,
|
|
6
|
-
# Subclasses get an HTTP client with retries and error
|
|
5
|
+
# its Financials, a Quote or +nil+, the analyst Estimates it has, and
|
|
6
|
+
# its PriceHistory. Subclasses get an HTTP client with retries and error
|
|
7
|
+
# mapping.
|
|
7
8
|
class Provider
|
|
8
9
|
class << self
|
|
9
10
|
# The symbol the provider is registered under.
|
|
@@ -34,6 +35,10 @@ module Fundamentalista
|
|
|
34
35
|
raise NotImplementedError
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
def prices(company, from:, to:)
|
|
39
|
+
raise NotImplementedError
|
|
40
|
+
end
|
|
41
|
+
|
|
37
42
|
private
|
|
38
43
|
|
|
39
44
|
def get(url, params = {}, headers: {})
|
|
@@ -21,9 +21,10 @@ module Fundamentalista
|
|
|
21
21
|
gross_profit: %w[GrossProfit],
|
|
22
22
|
sga: ['SellingGeneralAndAdministrativeExpense', %w[GeneralAndAdministrativeExpense SellingAndMarketingExpense]],
|
|
23
23
|
operating_income: %w[OperatingIncomeLoss ProfitLossFromOperatingActivities],
|
|
24
|
-
interest_expense:
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
interest_expense: ['InterestExpense', 'InterestExpenseNonoperating',
|
|
25
|
+
%w[InterestExpenseDebt FinanceLeaseInterestExpense],
|
|
26
|
+
'FinanceCosts', 'InterestExpenseOperating', 'InterestAndDebtExpense', 'InterestExpenseLongTermDebt',
|
|
27
|
+
'InterestExpenseBorrowings', 'InterestPaidNet'],
|
|
27
28
|
income_before_tax: %w[
|
|
28
29
|
IncomeLossFromContinuingOperationsBeforeIncomeTaxesExtraordinaryItemsNoncontrollingInterest
|
|
29
30
|
IncomeLossFromContinuingOperationsBeforeIncomeTaxesMinorityInterestAndIncomeLossFromEquityMethodInvestments
|
|
@@ -44,8 +45,8 @@ module Fundamentalista
|
|
|
44
45
|
cash: %w[CashAndCashEquivalentsAtCarryingValue CashCashEquivalentsRestrictedCashAndRestrictedCashEquivalents
|
|
45
46
|
CashAndCashEquivalents],
|
|
46
47
|
short_term_investments: %w[ShortTermInvestments MarketableSecuritiesCurrent
|
|
47
|
-
AvailableForSaleSecuritiesDebtSecuritiesCurrent
|
|
48
|
-
CurrentFinancialAssetsAtAmortisedCost],
|
|
48
|
+
AvailableForSaleSecuritiesDebtSecuritiesCurrent OtherShortTermInvestments
|
|
49
|
+
OtherCurrentFinancialAssets CurrentFinancialAssetsAtAmortisedCost],
|
|
49
50
|
receivables: %w[AccountsReceivableNetCurrent ReceivablesNetCurrent CurrentTradeReceivables
|
|
50
51
|
TradeAndOtherCurrentReceivables AccountsNotesAndLoansReceivableNetCurrent AccountsReceivableNet],
|
|
51
52
|
inventory: %w[InventoryNet Inventories],
|
|
@@ -60,14 +61,15 @@ module Fundamentalista
|
|
|
60
61
|
current_liabilities: %w[LiabilitiesCurrent CurrentLiabilities],
|
|
61
62
|
total_liabilities: %w[Liabilities],
|
|
62
63
|
short_term_debt: ['DebtCurrent',
|
|
63
|
-
%w[LongTermDebtCurrent CommercialPaper
|
|
64
|
-
ConvertibleDebtCurrent NotesPayableCurrent],
|
|
64
|
+
%w[LongTermDebtCurrent LongTermDebtAndCapitalLeaseObligationsCurrent CommercialPaper
|
|
65
|
+
ShortTermBorrowings OtherShortTermBorrowings ConvertibleDebtCurrent NotesPayableCurrent],
|
|
65
66
|
'ShorttermBorrowings', 'CurrentBorrowings'],
|
|
66
67
|
long_term_debt: %w[LongTermDebtNoncurrent LongTermDebtAndCapitalLeaseObligations LongTermDebt LongTermNotesPayable
|
|
67
68
|
LongTermNotesAndLoans LongtermBorrowings NoncurrentBorrowings
|
|
68
69
|
LongTermDebtAndCapitalLeaseObligationsIncludingCurrentMaturities],
|
|
69
70
|
equity: %w[StockholdersEquity StockholdersEquityIncludingPortionAttributableToNoncontrollingInterest
|
|
70
71
|
EquityAttributableToOwnersOfParent Equity],
|
|
72
|
+
minority_interest: %w[MinorityInterest NoncontrollingInterests],
|
|
71
73
|
retained_earnings: %w[RetainedEarningsAccumulatedDeficit RetainedEarnings],
|
|
72
74
|
shares_outstanding: %w[CommonStockSharesOutstanding NumberOfSharesOutstanding NumberOfSharesIssued
|
|
73
75
|
NumberOfSharesIssuedAndFullyPaid]
|
|
@@ -40,6 +40,10 @@ module Fundamentalista
|
|
|
40
40
|
[]
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
def prices(company, **)
|
|
44
|
+
raise QuoteUnavailableError, "EDGAR has no prices for #{company.ticker}; pass a PriceHistory"
|
|
45
|
+
end
|
|
46
|
+
|
|
43
47
|
private
|
|
44
48
|
|
|
45
49
|
STATEMENTS = { income: [IncomeStatement, Tags::INCOME], balance: [BalanceSheet, Tags::BALANCE],
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module Fundamentalista
|
|
4
4
|
module Providers
|
|
5
5
|
# Financial Modeling Prep. Needs Configuration#fmp_api_key. Serves
|
|
6
|
-
# annual and quarterly periods, company profiles, live
|
|
7
|
-
# analyst estimates.
|
|
6
|
+
# annual and quarterly periods, company profiles with beta, live
|
|
7
|
+
# quotes, analyst estimates and daily prices.
|
|
8
8
|
class FMP < Provider
|
|
9
9
|
BASE_URL = 'https://financialmodelingprep.com/stable/'
|
|
10
10
|
|
|
@@ -13,7 +13,7 @@ module Fundamentalista
|
|
|
13
13
|
raise CompanyNotFoundError, "FMP lists no company under #{ticker.inspect}" unless profile
|
|
14
14
|
|
|
15
15
|
Company.new(ticker: profile['symbol'], name: profile['companyName'], cik: profile['cik']&.to_i,
|
|
16
|
-
currency: profile['currency'], provider: self)
|
|
16
|
+
currency: profile['currency'], beta: profile['beta'], provider: self)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def financials(company, period: :annual, limit: 5)
|
|
@@ -32,6 +32,13 @@ module Fundamentalista
|
|
|
32
32
|
as_of: row['timestamp'] && Time.at(row['timestamp']))
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def prices(company, from:, to:)
|
|
36
|
+
params = { symbol: company.ticker, from: from.iso8601, to: to.iso8601 }
|
|
37
|
+
adjusted = fetch('historical-price-eod/dividend-adjusted', params).to_h { |row| [row['date'], row['adjClose']] }
|
|
38
|
+
points = fetch('historical-price-eod/light', params).map { |row| [row['date'], row['price'], adjusted[row['date']]] }
|
|
39
|
+
PriceHistory.new(points)
|
|
40
|
+
end
|
|
41
|
+
|
|
35
42
|
def estimates(company)
|
|
36
43
|
fetch('analyst-estimates', symbol: company.ticker, period: 'annual', limit: 10).map do |row|
|
|
37
44
|
Estimate.new(fiscal_year_end: Date.parse(row['date']), eps: row['epsAvg'], eps_low: row['epsLow'],
|
|
@@ -60,7 +67,7 @@ module Fundamentalista
|
|
|
60
67
|
IncomeStatement.new(
|
|
61
68
|
revenue: row['revenue'], cost_of_revenue: row['costOfRevenue'], gross_profit: row['grossProfit'],
|
|
62
69
|
sga: row['sellingGeneralAndAdministrativeExpenses'],
|
|
63
|
-
operating_income: row['operatingIncome'], interest_expense: row['interestExpense'],
|
|
70
|
+
operating_income: row['operatingIncome'], ebit: row['ebit'], interest_expense: row['interestExpense'],
|
|
64
71
|
income_before_tax: row['incomeBeforeTax'], income_tax: row['incomeTaxExpense'],
|
|
65
72
|
net_income: row['netIncome'], depreciation_amortization: row['depreciationAndAmortization'],
|
|
66
73
|
ebitda: row['ebitda'], eps_diluted: row['epsDiluted'], diluted_shares: row['weightedAverageShsOutDil']
|
|
@@ -75,7 +82,7 @@ module Fundamentalista
|
|
|
75
82
|
payables: row['accountPayables'],
|
|
76
83
|
current_liabilities: row['totalCurrentLiabilities'], total_liabilities: row['totalLiabilities'],
|
|
77
84
|
short_term_debt: row['shortTermDebt'], long_term_debt: row['longTermDebt'],
|
|
78
|
-
total_debt: row['totalDebt'], equity: row['totalStockholdersEquity'],
|
|
85
|
+
total_debt: row['totalDebt'], equity: row['totalStockholdersEquity'], minority_interest: row['minorityInterest'],
|
|
79
86
|
retained_earnings: row['retainedEarnings']
|
|
80
87
|
)
|
|
81
88
|
end
|
|
@@ -65,10 +65,10 @@ module Fundamentalista
|
|
|
65
65
|
Decimal.ratio(nopat, Decimal.sum(average(:total_debt), average(:equity)))
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
# Greenblatt's return on capital:
|
|
69
|
-
#
|
|
68
|
+
# Greenblatt's return on capital: EBIT over net working capital plus net
|
|
69
|
+
# fixed assets.
|
|
70
70
|
def return_on_capital
|
|
71
|
-
Decimal.ratio(income.
|
|
71
|
+
Decimal.ratio(income.ebit, Decimal.sum(average(:net_working_capital), average(:ppe)))
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def current_ratio
|
|
@@ -96,9 +96,9 @@ module Fundamentalista
|
|
|
96
96
|
Decimal.ratio(balance.net_debt, income.ebitda)
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
-
#
|
|
99
|
+
# EBIT over interest expense.
|
|
100
100
|
def interest_coverage
|
|
101
|
-
Decimal.ratio(income.
|
|
101
|
+
Decimal.ratio(income.ebit, income.interest_expense)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
# Average total assets over average equity, the leverage leg of DuPont.
|
|
@@ -59,7 +59,7 @@ module Fundamentalista
|
|
|
59
59
|
[
|
|
60
60
|
Decimal.ratio(balance.working_capital, assets),
|
|
61
61
|
Decimal.ratio(balance.retained_earnings, assets),
|
|
62
|
-
Decimal.ratio(period.income.
|
|
62
|
+
Decimal.ratio(period.income.ebit, assets),
|
|
63
63
|
Decimal.ratio(market_cap, balance.total_liabilities),
|
|
64
64
|
Decimal.ratio(period.income.revenue, assets)
|
|
65
65
|
]
|
|
@@ -69,9 +69,9 @@ module Fundamentalista
|
|
|
69
69
|
Decimal.ratio(enterprise_value, income.ebitda)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
# Enterprise value over
|
|
72
|
+
# Enterprise value over EBIT.
|
|
73
73
|
def ev_to_ebit
|
|
74
|
-
Decimal.ratio(enterprise_value, income.
|
|
74
|
+
Decimal.ratio(enterprise_value, income.ebit)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def ev_to_sales
|
|
@@ -91,10 +91,10 @@ module Fundamentalista
|
|
|
91
91
|
Decimal.ratio(income.eps_diluted, price)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
-
# Greenblatt's earnings yield:
|
|
95
|
-
#
|
|
94
|
+
# Greenblatt's earnings yield: EBIT over enterprise value. Rank it with
|
|
95
|
+
# Ratios#return_on_capital for the magic formula.
|
|
96
96
|
def ebit_yield
|
|
97
|
-
Decimal.ratio(income.
|
|
97
|
+
Decimal.ratio(income.ebit, enterprise_value)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def fcf_yield
|
|
@@ -151,6 +151,34 @@ module Fundamentalista
|
|
|
151
151
|
Decimal.ratio(Decimal.subtract(value, price), value)
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
+
# The cost of equity by the capital asset pricing model: the risk free
|
|
155
|
+
# rate plus +beta+ times the +equity_premium+. Rates are decimals.
|
|
156
|
+
def cost_of_equity(beta:, risk_free:, equity_premium: BigDecimal('0.05'))
|
|
157
|
+
Decimal.wrap(risk_free) + (Decimal.wrap(beta) * Decimal.wrap(equity_premium))
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# The pre-tax cost of debt: interest expense over total debt, or the
|
|
161
|
+
# +cost_of_debt+ you pass.
|
|
162
|
+
def cost_of_debt(cost_of_debt: nil)
|
|
163
|
+
cost_of_debt ? Decimal.wrap(cost_of_debt) : Decimal.ratio(income.interest_expense, balance.total_debt)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# The weighted average cost of capital at this market capitalization:
|
|
167
|
+
# equity at its CAPM cost, debt at its after tax cost, weighted by
|
|
168
|
+
# market value. A ready discount rate for #intrinsic_value.
|
|
169
|
+
#
|
|
170
|
+
# valuation.intrinsic_value(growth: 0.06, discount_rate: valuation.wacc(beta: 1.1, risk_free: 0.04))
|
|
171
|
+
#
|
|
172
|
+
def wacc(beta:, risk_free:, equity_premium: BigDecimal('0.05'), cost_of_debt: nil)
|
|
173
|
+
debt = balance.total_debt || BigDecimal('0')
|
|
174
|
+
capital = Decimal.sum(market_cap, debt)
|
|
175
|
+
return nil if capital.nil? || capital.zero?
|
|
176
|
+
|
|
177
|
+
equity_cost = cost_of_equity(beta: beta, risk_free: risk_free, equity_premium: equity_premium)
|
|
178
|
+
debt_cost = (self.cost_of_debt(cost_of_debt: cost_of_debt) || BigDecimal('0')) * (1 - (income.tax_rate || BigDecimal('0')))
|
|
179
|
+
((market_cap * equity_cost) + (debt * debt_cost)) / capital
|
|
180
|
+
end
|
|
181
|
+
|
|
154
182
|
# The reverse DCF: the yearly free cash flow growth the price implies
|
|
155
183
|
# under the given +discount_rate+ and the other DCF keywords. +nil+
|
|
156
184
|
# when no growth between -50% and +100% reproduces the price.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fundamentalista
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bruno Costanzo
|
|
@@ -93,6 +93,7 @@ files:
|
|
|
93
93
|
- lib/fundamentalista/income_statement.rb
|
|
94
94
|
- lib/fundamentalista/inspectable.rb
|
|
95
95
|
- lib/fundamentalista/period.rb
|
|
96
|
+
- lib/fundamentalista/price_history.rb
|
|
96
97
|
- lib/fundamentalista/provider.rb
|
|
97
98
|
- lib/fundamentalista/providers/edgar.rb
|
|
98
99
|
- lib/fundamentalista/providers/edgar/facts.rb
|