fundamentalista 0.4.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 +4 -0
- data/README.md +10 -1
- data/lib/fundamentalista/comparison.rb +31 -4
- data/lib/fundamentalista/provider.rb +6 -2
- data/lib/fundamentalista/providers/edgar.rb +4 -0
- data/lib/fundamentalista/providers/fmp.rb +10 -1
- data/lib/fundamentalista/rates.rb +43 -0
- data/lib/fundamentalista/valuation.rb +2 -1
- data/lib/fundamentalista/version.rb +1 -1
- data/lib/fundamentalista.rb +5 -4
- 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: 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
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
|
|
|
@@ -315,6 +323,7 @@ valuation.residual_income_value(cost_of_equity: 0.09, growth: 0.04, years: 10)
|
|
|
315
323
|
| Analyst estimates | No, pass an EPS | Yes |
|
|
316
324
|
| Prices and beta | No, bring a PriceHistory and a beta | Yes |
|
|
317
325
|
| Revenue segments | No | Yes |
|
|
326
|
+
| Exchange rates | No, bring Rates | Yes |
|
|
318
327
|
| Coverage | Companies filing with the SEC, US GAAP and IFRS, in their reporting currency | Global |
|
|
319
328
|
|
|
320
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.
|
|
@@ -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)
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module Fundamentalista
|
|
4
4
|
# The contract every data source implements: resolve a Company, return
|
|
5
5
|
# its Financials, a Quote or +nil+, the analyst Estimates it has, its
|
|
6
|
-
# PriceHistory and its revenue Segments
|
|
7
|
-
# with retries and error mapping.
|
|
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.
|
|
@@ -43,6 +43,10 @@ module Fundamentalista
|
|
|
43
43
|
raise NotImplementedError
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
def rates(currencies, to:)
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
end
|
|
49
|
+
|
|
46
50
|
private
|
|
47
51
|
|
|
48
52
|
def get(url, params = {}, headers: {})
|
|
@@ -48,6 +48,10 @@ module Fundamentalista
|
|
|
48
48
|
raise UnsupportedPeriodError, "EDGAR's company facts carry no segments for #{company.ticker}"
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
def rates(_currencies, **)
|
|
52
|
+
raise QuoteUnavailableError, 'EDGAR has no exchange rates; pass Rates'
|
|
53
|
+
end
|
|
54
|
+
|
|
51
55
|
private
|
|
52
56
|
|
|
53
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, daily prices
|
|
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
|
|
|
@@ -47,6 +48,14 @@ module Fundamentalista
|
|
|
47
48
|
Segments.new(rows.to_h { |row| [row['fiscalYear'].to_i, row['data'] || {}] })
|
|
48
49
|
end
|
|
49
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
|
+
|
|
50
59
|
def estimates(company)
|
|
51
60
|
fetch('analyst-estimates', symbol: company.ticker, period: 'annual', limit: 10).map do |row|
|
|
52
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
|
|
@@ -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
|
|
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,6 +101,7 @@ 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
|