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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 432e0a27a0bed71a2f55d3574bf27c0ac4a781d08217de34f2a9c9dfc3a4ea87
4
- data.tar.gz: d8c0b9a22759ec8142e016c072b2d59a153b014f3811284c00ca0e86b34771a1
3
+ metadata.gz: 36260d14ca8a99975cc3c9717ecd1fa5694f2e2d29144aaeb8d51e9eed3a25a5
4
+ data.tar.gz: 920282a4dd650e6ed172638e02b85c37b3ac6e0166af5f7a837b4e69003d1c59
5
5
  SHA512:
6
- metadata.gz: 8be12b7c9de524de12ec34b09d9216408d4c03211a2c63ff145132619ae5652ab74ce85009cf25f2ac9cbd91571328f6d7848250ae1f7265f237165a87276c2d
7
- data.tar.gz: bb4e2193d2f9ba6f76491f0ca66fc6bc5d11a253fddc1ccc1f8a61e46dee5956a8292a032c16a489b434086ea2a934d7f6c9e7fd313fb811c49812e0cc6c1412
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; everything else is in the reporting currency.
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 and equity when not reported.
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
- def initialize(ticker:, name:, provider:, cik: nil, currency: 'USD')
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
- # EBITDA, derived from operating income and depreciation when not reported.
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(operating_income, depreciation_amortization)
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+, and the analyst Estimates it has.
6
- # Subclasses get an HTTP client with retries and error mapping.
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: %w[InterestExpense InterestExpenseNonoperating InterestExpenseDebt FinanceCosts
25
- InterestExpenseOperating InterestAndDebtExpense InterestExpenseLongTermDebt
26
- InterestExpenseBorrowings InterestPaidNet],
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 OtherCurrentFinancialAssets
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 ShortTermBorrowings OtherShortTermBorrowings
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 quotes and
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: operating income over net working
69
- # capital plus net fixed assets.
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.operating_income, Decimal.sum(average(:net_working_capital), average(:ppe)))
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
- # Operating income over interest expense.
99
+ # EBIT over interest expense.
100
100
  def interest_coverage
101
- Decimal.ratio(income.operating_income, income.interest_expense)
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.operating_income, assets),
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 operating income.
72
+ # Enterprise value over EBIT.
73
73
  def ev_to_ebit
74
- Decimal.ratio(enterprise_value, income.operating_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: operating income over enterprise value.
95
- # Rank it with Ratios#return_on_capital for the magic formula.
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.operating_income, enterprise_value)
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.
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Fundamentalista
4
4
  # The version of the fundamentalista gem, as a string.
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
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.1.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