fundamentalista 0.3.0 → 0.5.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 +9 -0
- data/README.md +31 -1
- data/lib/fundamentalista/company.rb +8 -0
- data/lib/fundamentalista/comparison.rb +31 -4
- data/lib/fundamentalista/provider.rb +11 -3
- data/lib/fundamentalista/providers/edgar.rb +8 -0
- data/lib/fundamentalista/providers/fmp.rb +18 -1
- data/lib/fundamentalista/rates.rb +43 -0
- data/lib/fundamentalista/segments.rb +69 -0
- data/lib/fundamentalista/valuation.rb +34 -1
- data/lib/fundamentalista/version.rb +1 -1
- data/lib/fundamentalista.rb +5 -4
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 60ef7c2913952daa4c9a4bcf633813d421326b9cc9ccc55755731ef536c51785
|
|
4
|
+
data.tar.gz: 712549d9e3a33d73e2ce21ca19c5780267cdb64a02261b546dff0e62ac05c3e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5d017ed989ad8ba888720041675e15bd683fdf391dcfb9dbdf5d14918f9acc88581854eeef5ed4995f69b3ad25074f5e60f1df63e420e03481ed387b342a302
|
|
7
|
+
data.tar.gz: f839a5b66257300a5b139fac8d303b031ebfd282878b16bdaa6a79dd643d59a905b89499d1ab93195f623190b74d5c66cd5cf4fe36792ceddde8c4c4e0af364b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
- Exchange `Rates` from FMP or your own, and comparisons in one currency that convert amounts and leave ratios alone.
|
|
6
|
+
|
|
7
|
+
## 0.4.0
|
|
8
|
+
|
|
9
|
+
- Revenue by product line and by region from FMP as `Segments`, with shares and growth per segment.
|
|
10
|
+
- Greenwald's earnings power value and the residual income model on the valuation.
|
|
11
|
+
|
|
3
12
|
## 0.3.0
|
|
4
13
|
|
|
5
14
|
- Insurance lines and ratios: premiums, claims, loss, expense and combined ratios, investment yield and the float, read from the concepts property and casualty insurers report.
|
data/README.md
CHANGED
|
@@ -190,7 +190,15 @@ peers.rank(:pe, ascending: true)
|
|
|
190
190
|
peers.magic_formula
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
-
Tickers the provider cannot resolve are logged and left out, so one bad symbol does not spoil a screen.
|
|
193
|
+
Tickers the provider cannot resolve are logged and left out, so one bad symbol does not spoil a screen.
|
|
194
|
+
|
|
195
|
+
Ratios and multiples compare across currencies as they are; amounts do not. Pass `currency:` and the amounts, revenue, market cap, book value per share and the rest, are converted through exchange `Rates`, fetched from FMP or given by you on EDGAR, while everything that is a rate or a multiple stays untouched.
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
rates = Fundamentalista::Rates.new({ "DKK" => 0.156, "EUR" => 1.16 }, to: "USD")
|
|
199
|
+
Fundamentalista.compare("AAPL", "NVO", "SAP", currency: "USD", rates: rates).values(:revenue)
|
|
200
|
+
Fundamentalista.compare("AAPL", "NVO", currency: "USD", provider: :fmp).table(:revenue, :net_margin) # rates from FMP
|
|
201
|
+
```
|
|
194
202
|
|
|
195
203
|
## Provenance
|
|
196
204
|
|
|
@@ -285,6 +293,26 @@ company.valuation_history(:pe, :pb, :fcf_yield) # {2025 => {price:
|
|
|
285
293
|
company.valuation_history(:pe, prices: prices) # with a history you bring
|
|
286
294
|
```
|
|
287
295
|
|
|
296
|
+
## Segments
|
|
297
|
+
|
|
298
|
+
FMP serves revenue by product line and by region for every fiscal year, as `Segments`: what the company sells and where, and how each part grows. EDGAR's company facts carry no segments.
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
segments = company.segments(by: :product) # or :geography
|
|
302
|
+
segments.latest # {"iPhone" => ..., "Service" => ...}
|
|
303
|
+
segments.share # each segment's share of the latest year
|
|
304
|
+
segments.growth("Service", years: 5) # CAGR of one segment
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### More valuation models
|
|
308
|
+
|
|
309
|
+
Two models complement the DCF. Greenwald's earnings power value capitalizes today's after tax EBIT at the discount rate as if the company never grew, so the gap to the price is the growth being paid for. The residual income model starts from book value and adds only the earnings above the cost of equity's charge on it, so it rewards returns on equity above that cost and nothing else.
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
valuation.earnings_power_value(discount_rate: 0.09)
|
|
313
|
+
valuation.residual_income_value(cost_of_equity: 0.09, growth: 0.04, years: 10)
|
|
314
|
+
```
|
|
315
|
+
|
|
288
316
|
## Providers
|
|
289
317
|
|
|
290
318
|
| | EDGAR | Financial Modeling Prep |
|
|
@@ -294,6 +322,8 @@ company.valuation_history(:pe, prices: prices) # with a history y
|
|
|
294
322
|
| Quotes | No, pass a price | Yes |
|
|
295
323
|
| Analyst estimates | No, pass an EPS | Yes |
|
|
296
324
|
| Prices and beta | No, bring a PriceHistory and a beta | Yes |
|
|
325
|
+
| Revenue segments | No | Yes |
|
|
326
|
+
| Exchange rates | No, bring Rates | Yes |
|
|
297
327
|
| Coverage | Companies filing with the SEC, US GAAP and IFRS, in their reporting currency | Global |
|
|
298
328
|
|
|
299
329
|
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.
|
|
@@ -12,6 +12,7 @@ module Fundamentalista
|
|
|
12
12
|
# company.valuation(price: 320).pe
|
|
13
13
|
# company.estimate.eps # FMP only
|
|
14
14
|
# company.valuation_history(:pe) # {2025 => {price:, pe:}, ...}
|
|
15
|
+
# company.segments(by: :geography).share # FMP only
|
|
15
16
|
#
|
|
16
17
|
class Company
|
|
17
18
|
include Inspectable
|
|
@@ -65,6 +66,13 @@ module Fundamentalista
|
|
|
65
66
|
end.to_h
|
|
66
67
|
end
|
|
67
68
|
|
|
69
|
+
# Returns the revenue Segments by +:product+ or +:geography+. Raises
|
|
70
|
+
# UnsupportedPeriodError on EDGAR, whose company facts carry none.
|
|
71
|
+
def segments(by: :product)
|
|
72
|
+
@segments ||= {}
|
|
73
|
+
@segments[by] ||= provider.segments(self, by: by)
|
|
74
|
+
end
|
|
75
|
+
|
|
68
76
|
# Returns the trailing twelve months as a Period, built from the last
|
|
69
77
|
# +quarters+ quarterly periods; eight give it a prior period too.
|
|
70
78
|
def ttm(quarters: 8)
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
module Fundamentalista
|
|
4
4
|
# Several companies side by side: any ratio, valuation figure, score or
|
|
5
5
|
# line item as a table, rankings on any of them, and Greenblatt's magic
|
|
6
|
-
# formula ranking.
|
|
6
|
+
# formula ranking. With a +currency+, amounts such as revenue or market
|
|
7
|
+
# cap are converted into it through Rates, while ratios and multiples
|
|
8
|
+
# stay as they are.
|
|
7
9
|
#
|
|
8
10
|
# peers = Fundamentalista.compare("AAPL", "MSFT", prices: { "AAPL" => 320, "MSFT" => 500 })
|
|
9
11
|
# peers.table(:roe, :pe, :piotroski) # => {"AAPL" => {roe: ..., pe: ..., piotroski: 8}, ...}
|
|
10
12
|
# peers.rank(:pe, ascending: true) # => ["MSFT", "AAPL"]
|
|
11
13
|
# peers.magic_formula # => ["AAPL", "MSFT"]
|
|
14
|
+
# Fundamentalista.compare("AAPL", "NVO", currency: "USD").values(:revenue)
|
|
12
15
|
#
|
|
13
16
|
class Comparison
|
|
14
17
|
include Enumerable
|
|
@@ -20,9 +23,25 @@ module Fundamentalista
|
|
|
20
23
|
|
|
21
24
|
attr_reader :companies
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
# The currency amounts are converted into, or +nil+ to leave each
|
|
27
|
+
# company in its own.
|
|
28
|
+
attr_reader :currency
|
|
29
|
+
|
|
30
|
+
def initialize(companies, prices: {}, currency: nil, rates: nil)
|
|
24
31
|
@companies = companies.to_h { |company| [company.ticker, company] }
|
|
25
32
|
@prices = prices.transform_keys(&:to_s)
|
|
33
|
+
@currency = currency
|
|
34
|
+
@rates = rates
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The Rates in use, fetched from the first company's provider when
|
|
38
|
+
# none were given.
|
|
39
|
+
def rates
|
|
40
|
+
return nil unless currency
|
|
41
|
+
|
|
42
|
+
@rates ||= companies.values.first.provider.rates(companies.values.map do |company|
|
|
43
|
+
company.latest_period.currency
|
|
44
|
+
end, to: currency)
|
|
26
45
|
end
|
|
27
46
|
|
|
28
47
|
def each(&)
|
|
@@ -67,10 +86,18 @@ module Fundamentalista
|
|
|
67
86
|
period = company.latest_period
|
|
68
87
|
valuation = valuation_of(company, period)
|
|
69
88
|
return period.ratios.public_send(name) if Ratios::ALL.include?(name)
|
|
70
|
-
|
|
89
|
+
if Valuation::ALL.include?(name)
|
|
90
|
+
return converted(valuation&.public_send(name), period, amount: Valuation::AMOUNTS.include?(name))
|
|
91
|
+
end
|
|
71
92
|
return SCORES[name].call(period, valuation) if SCORES.key?(name)
|
|
72
93
|
|
|
73
|
-
period.metric(name)
|
|
94
|
+
converted(period.metric(name), period, amount: !name.end_with?('shares_outstanding', 'diluted_shares'))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def converted(value, period, amount:)
|
|
98
|
+
return value if value.nil? || !amount || currency.nil? || period.currency == currency
|
|
99
|
+
|
|
100
|
+
rates.convert(value, from: period.currency)
|
|
74
101
|
end
|
|
75
102
|
|
|
76
103
|
def valuation_of(company, period)
|
|
@@ -2,9 +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+, the analyst Estimates it has,
|
|
6
|
-
#
|
|
7
|
-
# mapping.
|
|
5
|
+
# its Financials, a Quote or +nil+, the analyst Estimates it has, its
|
|
6
|
+
# PriceHistory and its revenue Segments, and exchange Rates. Subclasses
|
|
7
|
+
# get an HTTP client with retries and error mapping.
|
|
8
8
|
class Provider
|
|
9
9
|
class << self
|
|
10
10
|
# The symbol the provider is registered under.
|
|
@@ -39,6 +39,14 @@ module Fundamentalista
|
|
|
39
39
|
raise NotImplementedError
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
def segments(company, by:)
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def rates(currencies, to:)
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
end
|
|
49
|
+
|
|
42
50
|
private
|
|
43
51
|
|
|
44
52
|
def get(url, params = {}, headers: {})
|
|
@@ -44,6 +44,14 @@ module Fundamentalista
|
|
|
44
44
|
raise QuoteUnavailableError, "EDGAR has no prices for #{company.ticker}; pass a PriceHistory"
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
def segments(company, **)
|
|
48
|
+
raise UnsupportedPeriodError, "EDGAR's company facts carry no segments for #{company.ticker}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def rates(_currencies, **)
|
|
52
|
+
raise QuoteUnavailableError, 'EDGAR has no exchange rates; pass Rates'
|
|
53
|
+
end
|
|
54
|
+
|
|
47
55
|
private
|
|
48
56
|
|
|
49
57
|
STATEMENTS = { income: [IncomeStatement, Tags::INCOME], balance: [BalanceSheet, Tags::BALANCE],
|
|
@@ -4,7 +4,8 @@ module Fundamentalista
|
|
|
4
4
|
module Providers
|
|
5
5
|
# Financial Modeling Prep. Needs Configuration#fmp_api_key. Serves
|
|
6
6
|
# annual and quarterly periods, company profiles with beta, live
|
|
7
|
-
# quotes, analyst estimates
|
|
7
|
+
# quotes, analyst estimates, daily prices, revenue segments and
|
|
8
|
+
# exchange rates.
|
|
8
9
|
class FMP < Provider
|
|
9
10
|
BASE_URL = 'https://financialmodelingprep.com/stable/'
|
|
10
11
|
|
|
@@ -39,6 +40,22 @@ module Fundamentalista
|
|
|
39
40
|
PriceHistory.new(points)
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
SEGMENTS = { product: 'revenue-product-segmentation', geography: 'revenue-geographic-segments' }.freeze
|
|
44
|
+
|
|
45
|
+
def segments(company, by: :product)
|
|
46
|
+
endpoint = SEGMENTS.fetch(by) { raise ArgumentError, "Segments come by :product or :geography, not #{by.inspect}" }
|
|
47
|
+
rows = fetch(endpoint, symbol: company.ticker, period: 'annual')
|
|
48
|
+
Segments.new(rows.to_h { |row| [row['fiscalYear'].to_i, row['data'] || {}] })
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def rates(currencies, to: 'USD')
|
|
52
|
+
pairs = currencies.map(&:to_s).uniq - [to]
|
|
53
|
+
return Rates.new({}, to: to) if pairs.empty?
|
|
54
|
+
|
|
55
|
+
rows = fetch('batch-quote-short', symbols: pairs.map { |currency| "#{currency}#{to}" }.join(','))
|
|
56
|
+
Rates.new(rows.to_h { |row| [row['symbol'].delete_suffix(to), row['price']] }, to: to)
|
|
57
|
+
end
|
|
58
|
+
|
|
42
59
|
def estimates(company)
|
|
43
60
|
fetch('analyst-estimates', symbol: company.ticker, period: 'annual', limit: 10).map do |row|
|
|
44
61
|
Estimate.new(fiscal_year_end: Date.parse(row['date']), eps: row['epsAvg'], eps_low: row['epsLow'],
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fundamentalista
|
|
4
|
+
# Exchange rates into one base currency: how many units of the base one
|
|
5
|
+
# unit of each currency buys. Converts amounts so companies reporting in
|
|
6
|
+
# different currencies can be put side by side.
|
|
7
|
+
#
|
|
8
|
+
# rates = Fundamentalista::Rates.new({ "EUR" => 1.16, "JPY" => 0.0068 }, to: "USD")
|
|
9
|
+
# rates.convert(36_800_000_000, from: "EUR") # => 0.42688e11
|
|
10
|
+
#
|
|
11
|
+
class Rates
|
|
12
|
+
include Inspectable
|
|
13
|
+
|
|
14
|
+
# The ISO code every amount converts into.
|
|
15
|
+
attr_reader :base
|
|
16
|
+
|
|
17
|
+
def initialize(rates, to: 'USD')
|
|
18
|
+
@base = to
|
|
19
|
+
@rates = rates.to_h { |currency, rate| [currency.to_s, Decimal.wrap(rate)] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns the rate of +currency+ into the base, 1 for the base itself,
|
|
23
|
+
# or +nil+ when unknown.
|
|
24
|
+
def [](currency)
|
|
25
|
+
currency.to_s == base ? BigDecimal('1') : @rates[currency.to_s]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def known?(currency)
|
|
29
|
+
!self[currency].nil?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns +amount+ in the base currency, or +nil+ when the amount or
|
|
33
|
+
# the rate is missing.
|
|
34
|
+
def convert(amount, from:)
|
|
35
|
+
rate = self[from]
|
|
36
|
+
amount && rate && (Decimal.wrap(amount) * rate)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def inspect_attributes # :nodoc:
|
|
40
|
+
{ base: base, currencies: @rates.keys }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fundamentalista
|
|
4
|
+
# Revenue by product line or by region over the years: what a company
|
|
5
|
+
# actually sells and where. Amounts are in the reporting currency,
|
|
6
|
+
# keyed by fiscal year, newest first.
|
|
7
|
+
#
|
|
8
|
+
# segments = company.segments(by: :product) # FMP
|
|
9
|
+
# segments.latest # => {"iPhone" => 0.209586e12, "Service" => 0.109158e12, ...}
|
|
10
|
+
# segments.share # => {"iPhone" => 0.503e0, ...}
|
|
11
|
+
# segments.growth("Service", years: 5)
|
|
12
|
+
#
|
|
13
|
+
class Segments
|
|
14
|
+
include Inspectable
|
|
15
|
+
include Serializable
|
|
16
|
+
|
|
17
|
+
def initialize(years)
|
|
18
|
+
@years = years.sort_by { |year, _| -year }.to_h do |year, amounts|
|
|
19
|
+
[year, amounts.transform_values do |amount|
|
|
20
|
+
Decimal.wrap(amount)
|
|
21
|
+
end]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The fiscal years on file, newest first.
|
|
26
|
+
def years
|
|
27
|
+
@years.keys
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Every segment name that appears in any year.
|
|
31
|
+
def names
|
|
32
|
+
@years.values.flat_map(&:keys).uniq
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The amounts of +year+, the latest by default.
|
|
36
|
+
def [](year)
|
|
37
|
+
@years[year]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def latest
|
|
41
|
+
@years.values.first
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns each segment's share of the year's total, as rates.
|
|
45
|
+
def share(year = years.first)
|
|
46
|
+
amounts = @years.fetch(year, {})
|
|
47
|
+
total = amounts.values.sum(BigDecimal('0'))
|
|
48
|
+
amounts.transform_values { |amount| Decimal.ratio(amount, total) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns the compound annual growth of +name+ from the year +years+
|
|
52
|
+
# back to the latest, or over the whole history by default.
|
|
53
|
+
def growth(name, years: nil)
|
|
54
|
+
span = years ? [years, self.years.size - 1].min : self.years.size - 1
|
|
55
|
+
return nil if span < 1
|
|
56
|
+
|
|
57
|
+
first = self.years[span]
|
|
58
|
+
Decimal.cagr(@years.dig(first, name), @years.dig(self.years.first, name), self.years.first - first)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_h
|
|
62
|
+
@years
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def inspect_attributes # :nodoc:
|
|
66
|
+
{ years: years.size, latest: years.first, segments: names.size }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -18,7 +18,8 @@ module Fundamentalista
|
|
|
18
18
|
|
|
19
19
|
MULTIPLES = %i[pe forward_pe pb ps ev_to_ebitda ev_to_ebit ev_to_sales peg].freeze
|
|
20
20
|
YIELDS = %i[earnings_yield ebit_yield fcf_yield dividend_yield shareholder_yield payout_ratio].freeze
|
|
21
|
-
|
|
21
|
+
AMOUNTS = %i[market_cap enterprise_value book_value_per_share graham_number].freeze
|
|
22
|
+
ALL = (MULTIPLES + YIELDS + AMOUNTS).freeze
|
|
22
23
|
|
|
23
24
|
attr_reader :period, :quote, :estimate
|
|
24
25
|
|
|
@@ -179,6 +180,38 @@ module Fundamentalista
|
|
|
179
180
|
((market_cap * equity_cost) + (debt * debt_cost)) / capital
|
|
180
181
|
end
|
|
181
182
|
|
|
183
|
+
# Greenwald's earnings power value per share: the after tax EBIT the
|
|
184
|
+
# business earns today, capitalized at the +discount_rate+ as if it
|
|
185
|
+
# never grew, net of debt. What the company is worth with no growth
|
|
186
|
+
# priced in; compare it with the price to see how much growth is.
|
|
187
|
+
def earnings_power_value(discount_rate:)
|
|
188
|
+
nopat = income.ebit && income.tax_rate && (income.ebit * (1 - income.tax_rate))
|
|
189
|
+
return nil if nopat.nil? || shares_outstanding.nil?
|
|
190
|
+
|
|
191
|
+
equity = (nopat / Decimal.wrap(discount_rate)) - (balance.net_debt || 0)
|
|
192
|
+
equity / shares_outstanding
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# The residual income value per share: book value plus the present
|
|
196
|
+
# value of the earnings above the +cost_of_equity+ charge on it, grown
|
|
197
|
+
# at +growth+ for +years+ and then fading to nothing. Rewards returns
|
|
198
|
+
# on equity above the cost of equity, and nothing else.
|
|
199
|
+
def residual_income_value(cost_of_equity:, growth: BigDecimal('0'), years: 10)
|
|
200
|
+
book = balance.equity
|
|
201
|
+
earnings = income.net_income
|
|
202
|
+
return nil if book.nil? || earnings.nil? || shares_outstanding.nil?
|
|
203
|
+
|
|
204
|
+
rate = Decimal.wrap(cost_of_equity)
|
|
205
|
+
value = book
|
|
206
|
+
(1..years).each do |year|
|
|
207
|
+
residual = earnings - (rate * book)
|
|
208
|
+
value += residual / ((1 + rate)**year)
|
|
209
|
+
earnings *= 1 + Decimal.wrap(growth)
|
|
210
|
+
book += earnings * (1 - (payout_ratio || 0))
|
|
211
|
+
end
|
|
212
|
+
value / shares_outstanding
|
|
213
|
+
end
|
|
214
|
+
|
|
182
215
|
# The reverse DCF: the yearly free cash flow growth the price implies
|
|
183
216
|
# under the given +discount_rate+ and the other DCF keywords. +nil+
|
|
184
217
|
# when no growth between -50% and +100% reproduces the price.
|
data/lib/fundamentalista.rb
CHANGED
|
@@ -36,9 +36,10 @@ module Fundamentalista
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# Returns a Comparison of the companies behind +tickers+, valued at
|
|
39
|
-
# +prices+ where given
|
|
40
|
-
#
|
|
41
|
-
|
|
39
|
+
# +prices+ where given, with amounts converted into +currency+ through
|
|
40
|
+
# the provider's Rates or the +rates+ you pass. Tickers the provider
|
|
41
|
+
# cannot resolve are logged and left out.
|
|
42
|
+
def compare(*tickers, prices: {}, currency: nil, rates: nil, provider: config.default_provider)
|
|
42
43
|
source = provider(provider)
|
|
43
44
|
companies = tickers.flatten.filter_map do |ticker|
|
|
44
45
|
source.company(ticker)
|
|
@@ -46,7 +47,7 @@ module Fundamentalista
|
|
|
46
47
|
logger.warn("Skipping #{ticker}: #{e.message}")
|
|
47
48
|
nil
|
|
48
49
|
end
|
|
49
|
-
Comparison.new(companies, prices: prices)
|
|
50
|
+
Comparison.new(companies, prices: prices, currency: currency, rates: rates)
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
# Returns an instance of the provider registered under +slug+.
|
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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bruno Costanzo
|
|
@@ -101,10 +101,12 @@ files:
|
|
|
101
101
|
- lib/fundamentalista/providers/edgar/tags.rb
|
|
102
102
|
- lib/fundamentalista/providers/fmp.rb
|
|
103
103
|
- lib/fundamentalista/quote.rb
|
|
104
|
+
- lib/fundamentalista/rates.rb
|
|
104
105
|
- lib/fundamentalista/ratios.rb
|
|
105
106
|
- lib/fundamentalista/scores/altman_z.rb
|
|
106
107
|
- lib/fundamentalista/scores/beneish.rb
|
|
107
108
|
- lib/fundamentalista/scores/piotroski.rb
|
|
109
|
+
- lib/fundamentalista/segments.rb
|
|
108
110
|
- lib/fundamentalista/serializable.rb
|
|
109
111
|
- lib/fundamentalista/source.rb
|
|
110
112
|
- lib/fundamentalista/statement.rb
|