fundamentalista 0.1.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 +7 -0
- data/CHANGELOG.md +15 -0
- data/LICENSE +21 -0
- data/README.md +297 -0
- data/lib/fundamentalista/balance_sheet.rb +44 -0
- data/lib/fundamentalista/banking.rb +32 -0
- data/lib/fundamentalista/cash_flow_statement.rb +20 -0
- data/lib/fundamentalista/company.rb +81 -0
- data/lib/fundamentalista/comparison.rb +85 -0
- data/lib/fundamentalista/configuration.rb +71 -0
- data/lib/fundamentalista/dcf.rb +73 -0
- data/lib/fundamentalista/decimal.rb +57 -0
- data/lib/fundamentalista/error.rb +23 -0
- data/lib/fundamentalista/estimate.rb +38 -0
- data/lib/fundamentalista/financials.rb +103 -0
- data/lib/fundamentalista/income_statement.rb +40 -0
- data/lib/fundamentalista/inspectable.rb +26 -0
- data/lib/fundamentalista/period.rb +163 -0
- data/lib/fundamentalista/provider.rb +70 -0
- data/lib/fundamentalista/providers/edgar/facts.rb +172 -0
- data/lib/fundamentalista/providers/edgar/tags.rb +116 -0
- data/lib/fundamentalista/providers/edgar.rb +109 -0
- data/lib/fundamentalista/providers/fmp.rb +109 -0
- data/lib/fundamentalista/quote.rb +32 -0
- data/lib/fundamentalista/ratios.rb +231 -0
- data/lib/fundamentalista/scores/altman_z.rb +69 -0
- data/lib/fundamentalista/scores/beneish.rb +112 -0
- data/lib/fundamentalista/scores/piotroski.rb +111 -0
- data/lib/fundamentalista/serializable.rb +34 -0
- data/lib/fundamentalista/source.rb +27 -0
- data/lib/fundamentalista/statement.rb +49 -0
- data/lib/fundamentalista/valuation.rb +201 -0
- data/lib/fundamentalista/version.rb +6 -0
- data/lib/fundamentalista.rb +85 -0
- metadata +136 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fundamentalista
|
|
4
|
+
# The market's price of a Period: multiples, yields, the Graham number,
|
|
5
|
+
# the Altman Z-score and a discounted cash flow intrinsic value. Built
|
|
6
|
+
# from a Period and a Quote, plus the next year's analyst Estimate when
|
|
7
|
+
# there is one, which feeds the forward multiples.
|
|
8
|
+
#
|
|
9
|
+
# valuation = company.valuation(price: 320)
|
|
10
|
+
# valuation.pe # => 0.4289e2
|
|
11
|
+
# valuation.forward_pe # => 0.3625e2, with an estimate
|
|
12
|
+
# valuation.fcf_yield # => 0.021e-1
|
|
13
|
+
# valuation.intrinsic_value(growth: 0.06, discount_rate: 0.09)
|
|
14
|
+
#
|
|
15
|
+
class Valuation
|
|
16
|
+
include Inspectable
|
|
17
|
+
include Serializable
|
|
18
|
+
|
|
19
|
+
MULTIPLES = %i[pe forward_pe pb ps ev_to_ebitda ev_to_ebit ev_to_sales peg].freeze
|
|
20
|
+
YIELDS = %i[earnings_yield ebit_yield fcf_yield dividend_yield shareholder_yield payout_ratio].freeze
|
|
21
|
+
ALL = (MULTIPLES + YIELDS + %i[market_cap enterprise_value book_value_per_share graham_number]).freeze
|
|
22
|
+
|
|
23
|
+
attr_reader :period, :quote, :estimate
|
|
24
|
+
|
|
25
|
+
def initialize(period, quote, estimate: nil)
|
|
26
|
+
@period = period
|
|
27
|
+
@quote = quote
|
|
28
|
+
@estimate = estimate
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def price
|
|
32
|
+
quote.price
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Shares from the quote, else the balance sheet's count, else the
|
|
36
|
+
# diluted weighted average of the period.
|
|
37
|
+
def shares_outstanding
|
|
38
|
+
quote.shares_outstanding || balance.shares_outstanding || income.diluted_shares
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def market_cap
|
|
42
|
+
quote.market_cap || (shares_outstanding && (price * shares_outstanding))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Market capitalization plus net debt.
|
|
46
|
+
def enterprise_value
|
|
47
|
+
Decimal.sum(market_cap, balance.net_debt)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def pe
|
|
51
|
+
Decimal.ratio(price, income.eps_diluted)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Price over next year's EPS: the estimate's consensus, or the +eps+
|
|
55
|
+
# you pass. +nil+ without either.
|
|
56
|
+
def forward_pe(eps: estimate&.eps)
|
|
57
|
+
Decimal.ratio(price, Decimal.wrap(eps))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def pb
|
|
61
|
+
Decimal.ratio(price, book_value_per_share)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def ps
|
|
65
|
+
Decimal.ratio(market_cap, income.revenue)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def ev_to_ebitda
|
|
69
|
+
Decimal.ratio(enterprise_value, income.ebitda)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Enterprise value over operating income.
|
|
73
|
+
def ev_to_ebit
|
|
74
|
+
Decimal.ratio(enterprise_value, income.operating_income)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def ev_to_sales
|
|
78
|
+
Decimal.ratio(enterprise_value, income.revenue)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Price to earnings over expected earnings growth in percent points.
|
|
82
|
+
# The growth is the one the estimate implies over this period's EPS,
|
|
83
|
+
# else the growth of net income against the prior period, unless
|
|
84
|
+
# +growth+ is given as a rate.
|
|
85
|
+
def peg(growth: expected_growth)
|
|
86
|
+
rate = Decimal.wrap(growth)
|
|
87
|
+
Decimal.ratio(pe, rate && (rate * 100))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def earnings_yield
|
|
91
|
+
Decimal.ratio(income.eps_diluted, price)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Greenblatt's earnings yield: operating income over enterprise value.
|
|
95
|
+
# Rank it with Ratios#return_on_capital for the magic formula.
|
|
96
|
+
def ebit_yield
|
|
97
|
+
Decimal.ratio(income.operating_income, enterprise_value)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def fcf_yield
|
|
101
|
+
Decimal.ratio(cash_flow.free_cash_flow, market_cap)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def dividend_yield
|
|
105
|
+
Decimal.ratio(cash_flow.dividends_paid, market_cap)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Dividends plus buybacks over market capitalization.
|
|
109
|
+
def shareholder_yield
|
|
110
|
+
Decimal.ratio(cash_flow.shareholder_returns, market_cap)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def payout_ratio
|
|
114
|
+
Decimal.ratio(cash_flow.dividends_paid, income.net_income)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def book_value_per_share
|
|
118
|
+
Decimal.ratio(balance.equity, shares_outstanding)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Graham's ceiling for a defensive investor: the square root of 22.5
|
|
122
|
+
# times earnings per share times book value per share. +nil+ when
|
|
123
|
+
# either is negative.
|
|
124
|
+
def graham_number
|
|
125
|
+
eps = income.eps_diluted
|
|
126
|
+
bvps = book_value_per_share
|
|
127
|
+
return nil if eps.nil? || bvps.nil? || eps.negative? || bvps.negative?
|
|
128
|
+
|
|
129
|
+
(BigDecimal('22.5') * eps * bvps).sqrt(16)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The Altman Z-score at this market capitalization.
|
|
133
|
+
def altman_z
|
|
134
|
+
period.altman_z(market_cap: market_cap)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# The DCF value per share of the period's free cash flow, net of debt.
|
|
138
|
+
# Takes the same keywords as DCF.
|
|
139
|
+
def intrinsic_value(**options)
|
|
140
|
+
flow = cash_flow.free_cash_flow
|
|
141
|
+
return nil if flow.nil? || flow <= 0 || shares_outstanding.nil?
|
|
142
|
+
|
|
143
|
+
equity = DCF.new(cash_flow: flow, **options).value - (balance.net_debt || 0)
|
|
144
|
+
equity / shares_outstanding
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# How far the price sits below the intrinsic value, as a rate of that
|
|
148
|
+
# value; negative when the price is above it.
|
|
149
|
+
def margin_of_safety(**options)
|
|
150
|
+
value = intrinsic_value(**options)
|
|
151
|
+
Decimal.ratio(Decimal.subtract(value, price), value)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The reverse DCF: the yearly free cash flow growth the price implies
|
|
155
|
+
# under the given +discount_rate+ and the other DCF keywords. +nil+
|
|
156
|
+
# when no growth between -50% and +100% reproduces the price.
|
|
157
|
+
def implied_growth(discount_rate:, **options)
|
|
158
|
+
return nil if intrinsic_value(growth: 0, discount_rate: discount_rate, **options).nil?
|
|
159
|
+
|
|
160
|
+
low = BigDecimal('-0.5')
|
|
161
|
+
high = BigDecimal('1')
|
|
162
|
+
return nil unless values_at?(low, discount_rate, options) < price && values_at?(high, discount_rate, options) > price
|
|
163
|
+
|
|
164
|
+
40.times do
|
|
165
|
+
middle = (low + high) / 2
|
|
166
|
+
values_at?(middle, discount_rate, options) < price ? low = middle : high = middle
|
|
167
|
+
end
|
|
168
|
+
(low + high) / 2
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def to_h
|
|
172
|
+
ALL.to_h { |name| [name, public_send(name)] }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def inspect_attributes # :nodoc:
|
|
176
|
+
{ price: price, pe: pe, forward_pe: forward_pe, pb: pb, ev_to_ebitda: ev_to_ebitda, fcf_yield: fcf_yield }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
def expected_growth
|
|
182
|
+
estimate ? estimate.eps_growth(income.eps_diluted) : period.growth(:net_income)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def values_at?(growth, discount_rate, options)
|
|
186
|
+
intrinsic_value(growth: growth, discount_rate: discount_rate, **options)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def income
|
|
190
|
+
period.income
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def balance
|
|
194
|
+
period.balance
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def cash_flow
|
|
198
|
+
period.cash_flow
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bigdecimal'
|
|
4
|
+
require 'bigdecimal/util'
|
|
5
|
+
require 'date'
|
|
6
|
+
require 'faraday'
|
|
7
|
+
require 'faraday/retry'
|
|
8
|
+
require 'json'
|
|
9
|
+
require 'logger'
|
|
10
|
+
require 'uri'
|
|
11
|
+
require 'zeitwerk'
|
|
12
|
+
require 'fundamentalista/error'
|
|
13
|
+
|
|
14
|
+
loader = Zeitwerk::Loader.for_gem
|
|
15
|
+
loader.inflector.inflect('dcf' => 'DCF', 'fmp' => 'FMP')
|
|
16
|
+
loader.setup
|
|
17
|
+
|
|
18
|
+
# Fundamental analysis of listed companies: financial statements, ratios,
|
|
19
|
+
# scores and valuation, from the SEC's EDGAR or Financial Modeling Prep.
|
|
20
|
+
#
|
|
21
|
+
# Fundamentalista.configure { |config| config.edgar_user_agent = "Acme research@acme.com" }
|
|
22
|
+
#
|
|
23
|
+
# company = Fundamentalista.company("AAPL")
|
|
24
|
+
# year = company.financials.latest
|
|
25
|
+
# year.ratios.roe # => 0.15e1 (BigDecimal)
|
|
26
|
+
# year.piotroski.score # => 7
|
|
27
|
+
# company.valuation(price: 320).pe
|
|
28
|
+
# Fundamentalista.compare("AAPL", "MSFT").rank(:roe)
|
|
29
|
+
#
|
|
30
|
+
module Fundamentalista
|
|
31
|
+
class << self
|
|
32
|
+
# Returns the Company behind +ticker+, resolved through +provider+
|
|
33
|
+
# (+:edgar+ or +:fmp+, defaulting to the configured provider).
|
|
34
|
+
def company(ticker, provider: config.default_provider)
|
|
35
|
+
provider(provider).company(ticker)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns a Comparison of the companies behind +tickers+, valued at
|
|
39
|
+
# +prices+ where given. Tickers the provider cannot resolve are logged
|
|
40
|
+
# and left out.
|
|
41
|
+
def compare(*tickers, prices: {}, provider: config.default_provider)
|
|
42
|
+
source = provider(provider)
|
|
43
|
+
companies = tickers.flatten.filter_map do |ticker|
|
|
44
|
+
source.company(ticker)
|
|
45
|
+
rescue Error => e
|
|
46
|
+
logger.warn("Skipping #{ticker}: #{e.message}")
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
Comparison.new(companies, prices: prices)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns an instance of the provider registered under +slug+.
|
|
53
|
+
def provider(slug)
|
|
54
|
+
klass = providers.fetch(slug.to_sym) do
|
|
55
|
+
raise ConfigurationError, "Unknown provider #{slug.inspect}. Known providers: #{providers.keys.join(', ')}"
|
|
56
|
+
end
|
|
57
|
+
klass.new(config)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The registered providers, keyed by slug.
|
|
61
|
+
def providers
|
|
62
|
+
@providers ||= { edgar: Providers::Edgar, fmp: Providers::FMP }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The global Configuration.
|
|
66
|
+
def config
|
|
67
|
+
@config ||= Configuration.new
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Yields the global Configuration.
|
|
71
|
+
#
|
|
72
|
+
# Fundamentalista.configure do |config|
|
|
73
|
+
# config.fmp_api_key = ENV["FMP_API_KEY"]
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
76
|
+
def configure
|
|
77
|
+
yield config
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The configured logger.
|
|
81
|
+
def logger
|
|
82
|
+
config.logger
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fundamentalista
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Bruno Costanzo
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: bigdecimal
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: faraday-retry
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: zeitwerk
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '2.6'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '2.6'
|
|
68
|
+
description: Financial statements, ratios, Piotroski and Altman scores, multiples
|
|
69
|
+
and discounted cash flow valuation for listed companies, from the SEC EDGAR XBRL
|
|
70
|
+
API or Financial Modeling Prep. BigDecimal throughout, nil instead of exceptions
|
|
71
|
+
on incomplete filings, one API over every provider.
|
|
72
|
+
email:
|
|
73
|
+
- dev.bcostanzo@gmail.com
|
|
74
|
+
executables: []
|
|
75
|
+
extensions: []
|
|
76
|
+
extra_rdoc_files: []
|
|
77
|
+
files:
|
|
78
|
+
- CHANGELOG.md
|
|
79
|
+
- LICENSE
|
|
80
|
+
- README.md
|
|
81
|
+
- lib/fundamentalista.rb
|
|
82
|
+
- lib/fundamentalista/balance_sheet.rb
|
|
83
|
+
- lib/fundamentalista/banking.rb
|
|
84
|
+
- lib/fundamentalista/cash_flow_statement.rb
|
|
85
|
+
- lib/fundamentalista/company.rb
|
|
86
|
+
- lib/fundamentalista/comparison.rb
|
|
87
|
+
- lib/fundamentalista/configuration.rb
|
|
88
|
+
- lib/fundamentalista/dcf.rb
|
|
89
|
+
- lib/fundamentalista/decimal.rb
|
|
90
|
+
- lib/fundamentalista/error.rb
|
|
91
|
+
- lib/fundamentalista/estimate.rb
|
|
92
|
+
- lib/fundamentalista/financials.rb
|
|
93
|
+
- lib/fundamentalista/income_statement.rb
|
|
94
|
+
- lib/fundamentalista/inspectable.rb
|
|
95
|
+
- lib/fundamentalista/period.rb
|
|
96
|
+
- lib/fundamentalista/provider.rb
|
|
97
|
+
- lib/fundamentalista/providers/edgar.rb
|
|
98
|
+
- lib/fundamentalista/providers/edgar/facts.rb
|
|
99
|
+
- lib/fundamentalista/providers/edgar/tags.rb
|
|
100
|
+
- lib/fundamentalista/providers/fmp.rb
|
|
101
|
+
- lib/fundamentalista/quote.rb
|
|
102
|
+
- lib/fundamentalista/ratios.rb
|
|
103
|
+
- lib/fundamentalista/scores/altman_z.rb
|
|
104
|
+
- lib/fundamentalista/scores/beneish.rb
|
|
105
|
+
- lib/fundamentalista/scores/piotroski.rb
|
|
106
|
+
- lib/fundamentalista/serializable.rb
|
|
107
|
+
- lib/fundamentalista/source.rb
|
|
108
|
+
- lib/fundamentalista/statement.rb
|
|
109
|
+
- lib/fundamentalista/valuation.rb
|
|
110
|
+
- lib/fundamentalista/version.rb
|
|
111
|
+
homepage: https://github.com/bruno-costanzo/fundamentalista
|
|
112
|
+
licenses:
|
|
113
|
+
- MIT
|
|
114
|
+
metadata:
|
|
115
|
+
source_code_uri: https://github.com/bruno-costanzo/fundamentalista
|
|
116
|
+
changelog_uri: https://github.com/bruno-costanzo/fundamentalista/blob/main/CHANGELOG.md
|
|
117
|
+
bug_tracker_uri: https://github.com/bruno-costanzo/fundamentalista/issues
|
|
118
|
+
rubygems_mfa_required: 'true'
|
|
119
|
+
rdoc_options: []
|
|
120
|
+
require_paths:
|
|
121
|
+
- lib
|
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
123
|
+
requirements:
|
|
124
|
+
- - ">="
|
|
125
|
+
- !ruby/object:Gem::Version
|
|
126
|
+
version: 3.1.3
|
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0'
|
|
132
|
+
requirements: []
|
|
133
|
+
rubygems_version: 4.0.16
|
|
134
|
+
specification_version: 4
|
|
135
|
+
summary: Fundamental analysis of listed companies, the Ruby way.
|
|
136
|
+
test_files: []
|