fundamentalista-rails 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 +7 -0
- data/LICENSE +21 -0
- data/README.md +68 -0
- data/app/controllers/fundamentalista_rails/application_controller.rb +26 -0
- data/app/controllers/fundamentalista_rails/companies_controller.rb +18 -0
- data/app/controllers/fundamentalista_rails/comparisons_controller.rb +18 -0
- data/app/controllers/fundamentalista_rails/holdings_controller.rb +20 -0
- data/app/controllers/fundamentalista_rails/snapshots_controller.rb +12 -0
- data/app/controllers/fundamentalista_rails/watchlists_controller.rb +27 -0
- data/app/helpers/fundamentalista_rails/figures_helper.rb +44 -0
- data/app/models/fundamentalista_rails/application_record.rb +8 -0
- data/app/models/fundamentalista_rails/holding.rb +28 -0
- data/app/models/fundamentalista_rails/snapshot.rb +18 -0
- data/app/models/fundamentalista_rails/watchlist.rb +10 -0
- data/app/views/fundamentalista_rails/companies/index.html.erb +7 -0
- data/app/views/fundamentalista_rails/companies/show.html.erb +36 -0
- data/app/views/fundamentalista_rails/comparisons/show.html.erb +41 -0
- data/app/views/fundamentalista_rails/shared/_ratios.html.erb +23 -0
- data/app/views/fundamentalista_rails/shared/_scores.html.erb +16 -0
- data/app/views/fundamentalista_rails/shared/_series.html.erb +17 -0
- data/app/views/fundamentalista_rails/shared/_valuation.html.erb +16 -0
- data/app/views/fundamentalista_rails/watchlists/index.html.erb +19 -0
- data/app/views/fundamentalista_rails/watchlists/show.html.erb +29 -0
- data/app/views/layouts/fundamentalista_rails/application.html.erb +45 -0
- data/config/routes.rb +12 -0
- data/db/migrate/20260906000000_create_fundamentalista_rails_tables.rb +27 -0
- data/lib/fundamentalista-rails.rb +17 -0
- data/lib/fundamentalista_rails/cache.rb +19 -0
- data/lib/fundamentalista_rails/engine.rb +19 -0
- data/lib/fundamentalista_rails/version.rb +6 -0
- metadata +105 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 301e35254de56b06adcdbb3e168c70998e63ef0b22141c016e03206744929101
|
|
4
|
+
data.tar.gz: 715976e46da9f92cf579a9d546d91262c9712a157e23ff1ff9e96388dbd44445
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f35926ae82acf6835a76aa04672e668228874d6a925d9484c0d170a07fdea270507a3c54e248563ba3d6c5ebb0a1bd7c3f9543e42055ccf761df9731eacabeab
|
|
7
|
+
data.tar.gz: b47411050fea5cdb894eae1ef9920aebccce3078fc1b3a3eb92089bbc2a8a9b6cb6c5f0cb38f9e396a3960203e64e79c894474883c5c3d295d1cb5281b7ab82d
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Mountable engine with company pages, peer comparison with the magic formula, and watchlists.
|
|
6
|
+
- Holdings keep a price and take snapshots of ratios, valuation and scores, so a valuation history accumulates.
|
|
7
|
+
- The fundamentalista gem uses the application's cache store by default, one day per provider response.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bruno Costanzo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# fundamentalista-rails
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/fundamentalista-rails) [](https://github.com/bruno-costanzo/fundamentalista-rails/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Fundamental analysis pages for your Rails application, on top of the [fundamentalista](https://github.com/bruno-costanzo/fundamentalista) gem. Mount the engine and you get company pages with statements, ratios, scores and valuation, peer comparison with the magic formula, and watchlists whose valuation snapshots build a history over time.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "fundamentalista-rails"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bin/rails fundamentalista_rails:install:migrations
|
|
15
|
+
bin/rails db:migrate
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Mount the engine and configure the data source. The SEC asks for a User-Agent naming your application and a contact email; Financial Modeling Prep needs an API key.
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# config/routes.rb
|
|
22
|
+
mount FundamentalistaRails::Engine, at: "/fundamentals"
|
|
23
|
+
|
|
24
|
+
# config/initializers/fundamentalista.rb
|
|
25
|
+
Fundamentalista.configure do |config|
|
|
26
|
+
config.edgar_user_agent = "Acme Research research@acme.com"
|
|
27
|
+
config.fmp_api_key = ENV["FMP_API_KEY"] # optional
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The engine points the gem at `Rails.cache` with a one day expiry unless you set `config.cache` yourself, so a company page costs one download and a watchlist does not hammer the SEC. Put the engine behind whatever authentication your application uses; it ships none.
|
|
32
|
+
|
|
33
|
+
## Pages
|
|
34
|
+
|
|
35
|
+
| Path | What it does |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `/fundamentals` | Look a ticker up |
|
|
38
|
+
| `/fundamentals/companies/AAPL` | Five years of statements with growth, every ratio, Piotroski, Beneish and owner earnings |
|
|
39
|
+
| `/fundamentals/companies/AAPL?price=320` | The same, plus multiples, yields, Graham number, Altman Z-score, intrinsic value and implied growth at that price |
|
|
40
|
+
| `/fundamentals/comparison?tickers=AAPL+MSFT+KO` | Peers side by side, with prices for the multiples, ranked by the magic formula |
|
|
41
|
+
| `/fundamentals/watchlists` | Named lists of companies, each holding with the price you value it at |
|
|
42
|
+
|
|
43
|
+
The pages are server rendered with a small stylesheet of their own, so they fit any application and need no assets, JavaScript or Tailwind.
|
|
44
|
+
|
|
45
|
+
## Watchlists and snapshots
|
|
46
|
+
|
|
47
|
+
A watchlist holds companies with a price. Taking a snapshot of a holding stores that day's ratios, valuation and scores as JSON on a `Snapshot` record, so looking at a company regularly leaves you with its valuation history, which the underlying data sources never give you for free.
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
holding = FundamentalistaRails::Watchlist.find_by(name: "Quality").holdings.find_by(ticker: "AAPL")
|
|
51
|
+
holding.snapshot!
|
|
52
|
+
holding.snapshots.map { |snapshot| [snapshot.taken_on, snapshot.dig(:valuation, :pe)] }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The models are plain Active Record and yours to extend: schedule `snapshot!` from a job to build the history without visiting the page.
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
bundle install
|
|
61
|
+
bundle exec rake # request specs against a dummy app, and RuboCop
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Specs stub the SEC with recorded responses; no keys are needed.
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FundamentalistaRails
|
|
4
|
+
# Base controller of the engine: turns the gem's errors into flash
|
|
5
|
+
# messages and reads prices from parameters.
|
|
6
|
+
class ApplicationController < ActionController::Base
|
|
7
|
+
protect_from_forgery with: :exception
|
|
8
|
+
|
|
9
|
+
rescue_from Fundamentalista::CompanyNotFoundError, Fundamentalista::NoFinancialsError,
|
|
10
|
+
Fundamentalista::UnsupportedPeriodError do |error|
|
|
11
|
+
redirect_to root_path, alert: error.message
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
rescue_from Fundamentalista::ConfigurationError do |error|
|
|
15
|
+
render plain: error.message, status: :internal_server_error
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def price_param(value = params[:price])
|
|
21
|
+
value.presence && BigDecimal(value.to_s)
|
|
22
|
+
rescue ArgumentError
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FundamentalistaRails
|
|
4
|
+
# The search box and the company page.
|
|
5
|
+
class CompaniesController < ApplicationController
|
|
6
|
+
def index
|
|
7
|
+
redirect_to company_path(params[:ticker].to_s.strip) if params[:ticker].present?
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
@company = Fundamentalista.company(params[:ticker])
|
|
12
|
+
@financials = @company.financials(limit: 5)
|
|
13
|
+
@year = @company.latest_period
|
|
14
|
+
@price = price_param
|
|
15
|
+
@valuation = @company.valuation(price: @price) if @price
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FundamentalistaRails
|
|
4
|
+
# Peers side by side, ranked by the magic formula.
|
|
5
|
+
class ComparisonsController < ApplicationController
|
|
6
|
+
METRICS = %i[pe fcf_yield ebit_yield roe return_on_capital net_margin debt_to_equity piotroski altman_z beneish].freeze
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
@tickers = params[:tickers].to_s.split(/[\s,]+/).map(&:upcase).compact_blank
|
|
10
|
+
return if @tickers.empty?
|
|
11
|
+
|
|
12
|
+
@prices = @tickers.index_with { |ticker| price_param(params.dig(:prices, ticker)) }.compact
|
|
13
|
+
@comparison = Fundamentalista.compare(*@tickers, prices: @prices)
|
|
14
|
+
@table = @comparison.table(*METRICS)
|
|
15
|
+
@magic_formula = @comparison.magic_formula
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FundamentalistaRails
|
|
4
|
+
# Adds companies to a watchlist and removes them.
|
|
5
|
+
class HoldingsController < ApplicationController
|
|
6
|
+
def create
|
|
7
|
+
watchlist = Watchlist.find(params[:watchlist_id])
|
|
8
|
+
holding = watchlist.holdings.create!(params.require(:holding).permit(:ticker, :price))
|
|
9
|
+
redirect_to watchlist, notice: "#{holding.ticker} added"
|
|
10
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
11
|
+
redirect_to watchlist, alert: e.message
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def destroy
|
|
15
|
+
holding = Holding.find(params[:id])
|
|
16
|
+
holding.destroy!
|
|
17
|
+
redirect_to holding.watchlist, notice: "#{holding.ticker} removed"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FundamentalistaRails
|
|
4
|
+
# Takes a valuation snapshot of a holding.
|
|
5
|
+
class SnapshotsController < ApplicationController
|
|
6
|
+
def create
|
|
7
|
+
holding = Holding.find(params[:holding_id])
|
|
8
|
+
holding.snapshot!
|
|
9
|
+
redirect_to holding.watchlist, notice: "Snapshot of #{holding.ticker} taken"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FundamentalistaRails
|
|
4
|
+
# Lists, creates and removes watchlists.
|
|
5
|
+
class WatchlistsController < ApplicationController
|
|
6
|
+
def index
|
|
7
|
+
@watchlists = Watchlist.order(:name)
|
|
8
|
+
@watchlist = Watchlist.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def show
|
|
12
|
+
@watchlist = Watchlist.find(params[:id])
|
|
13
|
+
@holdings = @watchlist.holdings.includes(:snapshots).order(:ticker)
|
|
14
|
+
@holding = @holdings.build
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create
|
|
18
|
+
watchlist = Watchlist.create!(params.require(:watchlist).permit(:name))
|
|
19
|
+
redirect_to watchlist
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def destroy
|
|
23
|
+
Watchlist.find(params[:id]).destroy!
|
|
24
|
+
redirect_to watchlists_path, notice: 'Watchlist removed'
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FundamentalistaRails
|
|
4
|
+
# Formats the gem's figures for the page: rates as percentages, amounts
|
|
5
|
+
# in millions of the reporting currency, multiples with one decimal,
|
|
6
|
+
# and a dash where the filing has no figure.
|
|
7
|
+
module FiguresHelper
|
|
8
|
+
DASH = '–'
|
|
9
|
+
ACRONYMS = %w[roe roa roic ebitda ebit fcf pe ps pb ev peg sga capex eps].freeze
|
|
10
|
+
|
|
11
|
+
# Turns a metric name into a label, keeping the acronyms upper case.
|
|
12
|
+
def label(name)
|
|
13
|
+
name.to_s.humanize.gsub(/\b(#{ACRONYMS.join('|')})\b/i, &:upcase)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def percent(value)
|
|
17
|
+
value ? format('%.1f%%', value.to_f * 100) : DASH
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def multiple(value)
|
|
21
|
+
value ? format('%.1fx', value.to_f) : DASH
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def amount(value, currency = 'USD')
|
|
25
|
+
value ? "#{number_with_delimiter((value.to_f / 1_000_000).round)} M #{currency}" : DASH
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def number(value, decimals = 2)
|
|
29
|
+
value ? number_with_precision(value.to_f, precision: decimals, delimiter: ',') : DASH
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def days(value)
|
|
33
|
+
value ? "#{value.to_f.round} d" : DASH
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def signal(value)
|
|
37
|
+
case value
|
|
38
|
+
when true then '✓'
|
|
39
|
+
when false then '✗'
|
|
40
|
+
else DASH
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FundamentalistaRails
|
|
4
|
+
# One company on a Watchlist, with the price used for its valuation and
|
|
5
|
+
# the Snapshots taken of it over time.
|
|
6
|
+
class Holding < ApplicationRecord
|
|
7
|
+
belongs_to :watchlist
|
|
8
|
+
has_many :snapshots, -> { order(taken_on: :desc) }, dependent: :destroy, inverse_of: :holding
|
|
9
|
+
|
|
10
|
+
validates :ticker, presence: true, uniqueness: { scope: :watchlist_id }
|
|
11
|
+
|
|
12
|
+
before_validation { self.ticker = ticker.to_s.upcase.tr('.', '-') }
|
|
13
|
+
|
|
14
|
+
# The Company behind the ticker, through the default provider.
|
|
15
|
+
def company
|
|
16
|
+
@company ||= Fundamentalista.company(ticker)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Takes a Snapshot of the latest annual period at the holding's price.
|
|
20
|
+
def snapshot!
|
|
21
|
+
valuation = company.valuation(price: price)
|
|
22
|
+
snapshots.create!(taken_on: Date.current, price: price,
|
|
23
|
+
figures: { ratios: valuation.period.ratios.as_json, valuation: valuation.as_json,
|
|
24
|
+
scores: { piotroski: valuation.period.piotroski.score, altman_z: valuation.altman_z.as_json,
|
|
25
|
+
beneish: valuation.period.beneish.score&.to_f } })
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FundamentalistaRails
|
|
4
|
+
# The ratios, valuation and scores of a Holding on one day, so the
|
|
5
|
+
# history of a company's valuation accumulates as you look at it.
|
|
6
|
+
class Snapshot < ApplicationRecord
|
|
7
|
+
belongs_to :holding
|
|
8
|
+
|
|
9
|
+
attribute :figures, :json
|
|
10
|
+
|
|
11
|
+
validates :taken_on, :figures, presence: true
|
|
12
|
+
|
|
13
|
+
# Returns a figure by path, such as +:valuation, :pe+.
|
|
14
|
+
def dig(*path)
|
|
15
|
+
figures.dig(*path.map(&:to_s))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FundamentalistaRails
|
|
4
|
+
# A named list of companies to follow, each with the price you value it at.
|
|
5
|
+
class Watchlist < ApplicationRecord
|
|
6
|
+
has_many :holdings, dependent: :destroy
|
|
7
|
+
|
|
8
|
+
validates :name, presence: true
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<% content_for :title, "Look up a company" %>
|
|
2
|
+
<h1>Look up a company</h1>
|
|
3
|
+
<p class="muted">Financial statements, ratios, scores and valuation from the SEC's EDGAR or Financial Modeling Prep.</p>
|
|
4
|
+
<%= form_with url: root_path, method: :get, class: "inline" do |form| %>
|
|
5
|
+
<%= form.text_field :ticker, placeholder: "AAPL", "aria-label": "Ticker", autofocus: true %>
|
|
6
|
+
<%= form.button "Look up" %>
|
|
7
|
+
<% end %>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<% content_for :title, @company.ticker %>
|
|
2
|
+
<h1><%= @company.name %> <span class="muted"><%= @company.ticker %></span></h1>
|
|
3
|
+
<p class="muted">
|
|
4
|
+
Fiscal <%= @year.fiscal_year %>, ended <%= @year.ended_on %>, in <%= @year.currency %>.
|
|
5
|
+
<% if @company.cik %>CIK <%= @company.cik %>.<% end %>
|
|
6
|
+
<% if @year.bank? %>Bank.<% end %>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<%= form_with url: company_path(@company.ticker), method: :get, class: "inline" do |form| %>
|
|
10
|
+
<%= form.number_field :price, value: @price, step: "any", placeholder: "Price", "aria-label": "Price" %>
|
|
11
|
+
<%= form.button "Value at this price" %>
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
14
|
+
<div class="grid">
|
|
15
|
+
<section>
|
|
16
|
+
<h2>Statements</h2>
|
|
17
|
+
<%= render "fundamentalista_rails/shared/series", financials: @financials, currency: @year.currency %>
|
|
18
|
+
</section>
|
|
19
|
+
|
|
20
|
+
<section>
|
|
21
|
+
<h2>Ratios</h2>
|
|
22
|
+
<%= render "fundamentalista_rails/shared/ratios", ratios: @year.ratios, bank: @year.bank? %>
|
|
23
|
+
</section>
|
|
24
|
+
|
|
25
|
+
<section>
|
|
26
|
+
<h2>Scores</h2>
|
|
27
|
+
<%= render "fundamentalista_rails/shared/scores", period: @year, valuation: @valuation %>
|
|
28
|
+
</section>
|
|
29
|
+
|
|
30
|
+
<% if @valuation %>
|
|
31
|
+
<section>
|
|
32
|
+
<h2>Valuation at <%= number(@price) %></h2>
|
|
33
|
+
<%= render "fundamentalista_rails/shared/valuation", valuation: @valuation %>
|
|
34
|
+
</section>
|
|
35
|
+
<% end %>
|
|
36
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<% content_for :title, "Compare" %>
|
|
2
|
+
<h1>Compare companies</h1>
|
|
3
|
+
<%= form_with url: comparison_path, method: :get, class: "inline" do |form| %>
|
|
4
|
+
<%= form.text_field :tickers, value: @tickers&.join(" "), placeholder: "AAPL MSFT KO", "aria-label": "Tickers", size: 30 %>
|
|
5
|
+
<%= form.button "Compare" %>
|
|
6
|
+
<% end %>
|
|
7
|
+
<% if @tickers.present? %>
|
|
8
|
+
<p class="muted">Prices, for the valuation multiples:</p>
|
|
9
|
+
<%= form_with url: comparison_path, method: :get, class: "inline" do |form| %>
|
|
10
|
+
<%= form.hidden_field :tickers, value: @tickers.join(" ") %>
|
|
11
|
+
<% @tickers.each do |ticker| %>
|
|
12
|
+
<label><%= ticker %> <%= form.number_field "prices[#{ticker}]", value: @prices[ticker], step: "any", size: 8 %></label>
|
|
13
|
+
<% end %>
|
|
14
|
+
<%= form.button "Value" %>
|
|
15
|
+
<% end %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% if @table %>
|
|
18
|
+
<h2>Side by side</h2>
|
|
19
|
+
<table>
|
|
20
|
+
<thead><tr><th></th><% @table.each_key do |ticker| %><th><%= link_to ticker, company_path(ticker, price: @prices[ticker]) %></th><% end %></tr></thead>
|
|
21
|
+
<tbody>
|
|
22
|
+
<% FundamentalistaRails::ComparisonsController::METRICS.each do |metric| %>
|
|
23
|
+
<tr>
|
|
24
|
+
<td><%= label(metric) %></td>
|
|
25
|
+
<% @table.each_value do |row| %>
|
|
26
|
+
<% value = row[metric] %>
|
|
27
|
+
<td>
|
|
28
|
+
<% case metric
|
|
29
|
+
when :pe, :debt_to_equity then %><%= multiple(value) %><%
|
|
30
|
+
when :piotroski then %><%= value || "–" %><%
|
|
31
|
+
when :altman_z, :beneish then %><%= number(value) %><%
|
|
32
|
+
else %><%= percent(value) %><% end %>
|
|
33
|
+
</td>
|
|
34
|
+
<% end %>
|
|
35
|
+
</tr>
|
|
36
|
+
<% end %>
|
|
37
|
+
</tbody>
|
|
38
|
+
</table>
|
|
39
|
+
<h2>Magic formula</h2>
|
|
40
|
+
<p>Greenblatt's rank, best first: <strong><%= @magic_formula.join(" › ") %></strong></p>
|
|
41
|
+
<% end %>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<% groups = { "Profitability" => Fundamentalista::Ratios::PROFITABILITY, "Liquidity" => Fundamentalista::Ratios::LIQUIDITY,
|
|
2
|
+
"Leverage" => Fundamentalista::Ratios::LEVERAGE, "Efficiency" => Fundamentalista::Ratios::EFFICIENCY,
|
|
3
|
+
"Working capital" => Fundamentalista::Ratios::WORKING_CAPITAL, "Cash" => Fundamentalista::Ratios::CASH } %>
|
|
4
|
+
<% groups["Banking"] = Fundamentalista::Ratios::BANKING if bank %>
|
|
5
|
+
<% rates = Fundamentalista::Ratios::PROFITABILITY + %i[debt_to_assets fcf_margin capex_to_revenue accrual_ratio net_interest_margin efficiency_ratio cost_of_risk allowance_to_loans loan_to_deposit] %>
|
|
6
|
+
<table>
|
|
7
|
+
<tbody>
|
|
8
|
+
<% groups.each do |group, names| %>
|
|
9
|
+
<tr><th colspan="2" style="text-align:left"><%= group %></th></tr>
|
|
10
|
+
<% names.each do |name| %>
|
|
11
|
+
<% value = ratios.public_send(name) %>
|
|
12
|
+
<tr>
|
|
13
|
+
<td><%= label(name) %></td>
|
|
14
|
+
<td>
|
|
15
|
+
<% if Fundamentalista::Ratios::WORKING_CAPITAL.include?(name) %><%= days(value) %>
|
|
16
|
+
<% elsif rates.include?(name) %><%= percent(value) %>
|
|
17
|
+
<% else %><%= multiple(value) %><% end %>
|
|
18
|
+
</td>
|
|
19
|
+
</tr>
|
|
20
|
+
<% end %>
|
|
21
|
+
<% end %>
|
|
22
|
+
</tbody>
|
|
23
|
+
</table>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<% piotroski = period.piotroski %>
|
|
2
|
+
<% beneish = period.beneish %>
|
|
3
|
+
<table>
|
|
4
|
+
<tbody>
|
|
5
|
+
<tr><td>Piotroski F-score</td><td><strong><%= piotroski.score %></strong> / 9</td></tr>
|
|
6
|
+
<% piotroski.signals.each do |name, value| %>
|
|
7
|
+
<tr><td class="muted"> <%= label(name) %></td><td><%= signal(value) %></td></tr>
|
|
8
|
+
<% end %>
|
|
9
|
+
<tr><td>Beneish M-score</td><td><%= number(beneish.score) %> <span class="muted"><%= beneish.likely_manipulator? ? "flagged" : (beneish.score ? "clear" : "") %></span></td></tr>
|
|
10
|
+
<% if valuation %>
|
|
11
|
+
<% z = valuation.altman_z %>
|
|
12
|
+
<tr><td>Altman Z-score</td><td><%= number(z.score) %> <span class="zone-<%= z.zone %>"><%= z.zone %></span></td></tr>
|
|
13
|
+
<% end %>
|
|
14
|
+
<tr><td>Owner earnings</td><td><%= amount(period.owner_earnings, period.currency) %></td></tr>
|
|
15
|
+
</tbody>
|
|
16
|
+
</table>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<% metrics = { revenue: "Revenue", operating_income: "Operating income", net_income: "Net income", free_cash_flow: "Free cash flow", equity: "Equity", total_debt: "Total debt", eps_diluted: "Diluted EPS" } %>
|
|
2
|
+
<table>
|
|
3
|
+
<thead>
|
|
4
|
+
<tr><th></th><% financials.each do |period| %><th><%= period.fiscal_year %></th><% end %><th>CAGR</th></tr>
|
|
5
|
+
</thead>
|
|
6
|
+
<tbody>
|
|
7
|
+
<% metrics.each do |metric, label| %>
|
|
8
|
+
<tr>
|
|
9
|
+
<td><%= label %></td>
|
|
10
|
+
<% financials.each do |period| %>
|
|
11
|
+
<td><%= metric == :eps_diluted ? number(period.metric(metric)) : amount(period.metric(metric), currency) %></td>
|
|
12
|
+
<% end %>
|
|
13
|
+
<td><%= percent(financials.growth(metric)) %></td>
|
|
14
|
+
</tr>
|
|
15
|
+
<% end %>
|
|
16
|
+
</tbody>
|
|
17
|
+
</table>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<table>
|
|
2
|
+
<tbody>
|
|
3
|
+
<tr><td>Market cap</td><td><%= amount(valuation.market_cap, valuation.period.currency) %></td></tr>
|
|
4
|
+
<tr><td>Enterprise value</td><td><%= amount(valuation.enterprise_value, valuation.period.currency) %></td></tr>
|
|
5
|
+
<% Fundamentalista::Valuation::MULTIPLES.each do |name| %>
|
|
6
|
+
<tr><td><%= label(name) %></td><td><%= multiple(valuation.public_send(name)) %></td></tr>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% Fundamentalista::Valuation::YIELDS.each do |name| %>
|
|
9
|
+
<tr><td><%= label(name) %></td><td><%= percent(valuation.public_send(name)) %></td></tr>
|
|
10
|
+
<% end %>
|
|
11
|
+
<tr><td>Book value per share</td><td><%= number(valuation.book_value_per_share) %></td></tr>
|
|
12
|
+
<tr><td>Graham number</td><td><%= number(valuation.graham_number) %></td></tr>
|
|
13
|
+
<tr><td>Intrinsic value (6% growth, 9% discount)</td><td><%= number(valuation.intrinsic_value(growth: 0.06, discount_rate: 0.09)) %></td></tr>
|
|
14
|
+
<tr><td>Implied growth at 9% discount</td><td><%= percent(valuation.implied_growth(discount_rate: 0.09)) %></td></tr>
|
|
15
|
+
</tbody>
|
|
16
|
+
</table>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<% content_for :title, "Watchlists" %>
|
|
2
|
+
<h1>Watchlists</h1>
|
|
3
|
+
<%= form_with model: @watchlist, url: watchlists_path, class: "inline" do |form| %>
|
|
4
|
+
<%= form.text_field :name, placeholder: "Quality compounders", "aria-label": "Name" %>
|
|
5
|
+
<%= form.button "Create" %>
|
|
6
|
+
<% end %>
|
|
7
|
+
<% if @watchlists.any? %>
|
|
8
|
+
<table>
|
|
9
|
+
<tbody>
|
|
10
|
+
<% @watchlists.each do |watchlist| %>
|
|
11
|
+
<tr>
|
|
12
|
+
<td><%= link_to watchlist.name, watchlist %></td>
|
|
13
|
+
<td><%= pluralize(watchlist.holdings.size, "company") %></td>
|
|
14
|
+
<td><%= button_to "Remove", watchlist, method: :delete, class: "quiet" %></td>
|
|
15
|
+
</tr>
|
|
16
|
+
<% end %>
|
|
17
|
+
</tbody>
|
|
18
|
+
</table>
|
|
19
|
+
<% end %>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<% content_for :title, @watchlist.name %>
|
|
2
|
+
<h1><%= @watchlist.name %></h1>
|
|
3
|
+
<%= form_with model: [@watchlist, @holding], class: "inline" do |form| %>
|
|
4
|
+
<%= form.text_field :ticker, placeholder: "Ticker", "aria-label": "Ticker", size: 8 %>
|
|
5
|
+
<%= form.number_field :price, placeholder: "Price", "aria-label": "Price", step: "any", size: 8 %>
|
|
6
|
+
<%= form.button "Add" %>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% if @holdings.any? %>
|
|
9
|
+
<table>
|
|
10
|
+
<thead><tr><th>Company</th><th>Price</th><th>Snapshots</th><th>Latest P/E</th><th>Latest FCF yield</th><th>Piotroski</th><th></th></tr></thead>
|
|
11
|
+
<tbody>
|
|
12
|
+
<% @holdings.each do |holding| %>
|
|
13
|
+
<% latest = holding.snapshots.first %>
|
|
14
|
+
<tr>
|
|
15
|
+
<td><%= link_to holding.ticker, company_path(holding.ticker, price: holding.price) %></td>
|
|
16
|
+
<td><%= number(holding.price) %></td>
|
|
17
|
+
<td><%= holding.snapshots.size %><% if latest %> <span class="muted">last <%= latest.taken_on %></span><% end %></td>
|
|
18
|
+
<td><%= multiple(latest&.dig(:valuation, :pe)) %></td>
|
|
19
|
+
<td><%= percent(latest&.dig(:valuation, :fcf_yield)) %></td>
|
|
20
|
+
<td><%= latest&.dig(:scores, :piotroski) || "–" %></td>
|
|
21
|
+
<td>
|
|
22
|
+
<%= button_to "Snapshot", watchlist_holding_snapshots_path(@watchlist, holding), class: "quiet" %>
|
|
23
|
+
<%= button_to "Remove", watchlist_holding_path(@watchlist, holding), method: :delete, class: "quiet" %>
|
|
24
|
+
</td>
|
|
25
|
+
</tr>
|
|
26
|
+
<% end %>
|
|
27
|
+
</tbody>
|
|
28
|
+
</table>
|
|
29
|
+
<% end %>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title><%= content_for?(:title) ? "#{yield(:title)} · Fundamentalista" : "Fundamentalista" %></title>
|
|
7
|
+
<%= csrf_meta_tags %>
|
|
8
|
+
<style>
|
|
9
|
+
:root { color-scheme: light dark; --line: #d4d4d8; --muted: #71717a; --accent: #2563eb; }
|
|
10
|
+
body { margin: 0; font: 15px/1.5 system-ui, sans-serif; max-width: 72rem; padding: 1.5rem; margin-inline: auto; }
|
|
11
|
+
header.site { display: flex; gap: 1.5rem; align-items: baseline; border-bottom: 1px solid var(--line); padding-bottom: .75rem; margin-bottom: 1.5rem; }
|
|
12
|
+
header.site a { color: inherit; text-decoration: none; }
|
|
13
|
+
header.site strong { font-size: 1.1rem; }
|
|
14
|
+
h1 { font-size: 1.5rem; margin: 0 0 .25rem; }
|
|
15
|
+
h2 { font-size: 1.05rem; margin: 2rem 0 .5rem; }
|
|
16
|
+
.muted { color: var(--muted); }
|
|
17
|
+
table { border-collapse: collapse; width: 100%; font-variant-numeric: tabular-nums; }
|
|
18
|
+
th, td { text-align: right; padding: .35rem .6rem; border-bottom: 1px solid var(--line); }
|
|
19
|
+
th:first-child, td:first-child { text-align: left; }
|
|
20
|
+
thead th { font-weight: 600; color: var(--muted); }
|
|
21
|
+
form.inline { display: inline-flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
|
|
22
|
+
input[type=text], input[type=number] { font: inherit; padding: .35rem .5rem; border: 1px solid var(--line); border-radius: .375rem; background: transparent; color: inherit; }
|
|
23
|
+
button { font: inherit; padding: .35rem .8rem; border-radius: .375rem; border: 1px solid var(--accent); background: var(--accent); color: white; cursor: pointer; }
|
|
24
|
+
button.quiet { background: transparent; color: inherit; border-color: var(--line); }
|
|
25
|
+
.flash { padding: .5rem .75rem; border-radius: .375rem; border: 1px solid var(--line); margin-bottom: 1rem; }
|
|
26
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: 1.5rem 2.5rem; }
|
|
27
|
+
.zone-safe { color: #16a34a; } .zone-grey { color: #ca8a04; } .zone-distress { color: #dc2626; }
|
|
28
|
+
</style>
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<header class="site">
|
|
32
|
+
<strong><%= link_to "Fundamentalista", root_path %></strong>
|
|
33
|
+
<%= link_to "Compare", comparison_path %>
|
|
34
|
+
<%= link_to "Watchlists", watchlists_path %>
|
|
35
|
+
<%= form_with url: root_path, method: :get, class: "inline", style: "margin-left:auto" do |form| %>
|
|
36
|
+
<%= form.text_field :ticker, placeholder: "Ticker", "aria-label": "Ticker", size: 8 %>
|
|
37
|
+
<%= form.button "Look up" %>
|
|
38
|
+
<% end %>
|
|
39
|
+
</header>
|
|
40
|
+
<% flash.each do |kind, message| %>
|
|
41
|
+
<p class="flash" data-kind="<%= kind %>"><%= message %></p>
|
|
42
|
+
<% end %>
|
|
43
|
+
<%= yield %>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
FundamentalistaRails::Engine.routes.draw do
|
|
4
|
+
root to: 'companies#index'
|
|
5
|
+
resources :companies, only: :show, param: :ticker
|
|
6
|
+
resource :comparison, only: :show
|
|
7
|
+
resources :watchlists, only: %i[index create show destroy] do
|
|
8
|
+
resources :holdings, only: %i[create destroy] do
|
|
9
|
+
resources :snapshots, only: :create
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateFundamentalistaRailsTables < ActiveRecord::Migration[7.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :fundamentalista_rails_watchlists do |t|
|
|
6
|
+
t.string :name, null: false
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
create_table :fundamentalista_rails_holdings do |t|
|
|
11
|
+
t.references :watchlist, null: false, foreign_key: { to_table: :fundamentalista_rails_watchlists }
|
|
12
|
+
t.string :ticker, null: false
|
|
13
|
+
t.decimal :price, precision: 20, scale: 6
|
|
14
|
+
t.timestamps
|
|
15
|
+
t.index %i[watchlist_id ticker], unique: true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
create_table :fundamentalista_rails_snapshots do |t|
|
|
19
|
+
t.references :holding, null: false, foreign_key: { to_table: :fundamentalista_rails_holdings }
|
|
20
|
+
t.date :taken_on, null: false
|
|
21
|
+
t.decimal :price, precision: 20, scale: 6
|
|
22
|
+
t.text :figures, null: false
|
|
23
|
+
t.timestamps
|
|
24
|
+
t.index %i[holding_id taken_on]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fundamentalista'
|
|
4
|
+
require 'rails/engine'
|
|
5
|
+
require 'fundamentalista_rails/version'
|
|
6
|
+
require 'fundamentalista_rails/cache'
|
|
7
|
+
require 'fundamentalista_rails/engine'
|
|
8
|
+
|
|
9
|
+
# Fundamental analysis inside a Rails application: an engine with company
|
|
10
|
+
# pages, peer comparison and watchlists that keep valuation snapshots,
|
|
11
|
+
# on top of the fundamentalista gem.
|
|
12
|
+
#
|
|
13
|
+
# # config/routes.rb
|
|
14
|
+
# mount FundamentalistaRails::Engine, at: "/fundamentals"
|
|
15
|
+
#
|
|
16
|
+
module FundamentalistaRails
|
|
17
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FundamentalistaRails
|
|
4
|
+
# Keeps provider responses in the application's cache store for a day,
|
|
5
|
+
# so a company page costs one download and a screen over a watchlist
|
|
6
|
+
# does not hammer the SEC.
|
|
7
|
+
class Cache
|
|
8
|
+
attr_reader :store, :expires_in
|
|
9
|
+
|
|
10
|
+
def initialize(store, expires_in: 1.day)
|
|
11
|
+
@store = store
|
|
12
|
+
@expires_in = expires_in
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def fetch(key, &)
|
|
16
|
+
store.fetch(key, expires_in: expires_in, &)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FundamentalistaRails
|
|
4
|
+
# Mounts the company pages, comparisons and watchlists, and points the
|
|
5
|
+
# fundamentalista gem at the application's cache unless one is set.
|
|
6
|
+
#
|
|
7
|
+
# mount FundamentalistaRails::Engine, at: "/fundamentals"
|
|
8
|
+
#
|
|
9
|
+
# Configure the gem in an initializer as usual; the engine only adds
|
|
10
|
+
# the cache and the migrations for watchlists.
|
|
11
|
+
class Engine < ::Rails::Engine
|
|
12
|
+
isolate_namespace FundamentalistaRails
|
|
13
|
+
|
|
14
|
+
config.after_initialize do
|
|
15
|
+
Fundamentalista.config.cache ||= Cache.new(::Rails.cache) if ::Rails.cache
|
|
16
|
+
Fundamentalista.config.logger = ::Rails.logger
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fundamentalista-rails
|
|
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: fundamentalista
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.1'
|
|
40
|
+
description: 'A mountable engine on top of the fundamentalista gem: look up any listed
|
|
41
|
+
company, read its statements, ratios, scores and valuation, compare peers with the
|
|
42
|
+
magic formula, and keep watchlists whose valuation snapshots build a history. Uses
|
|
43
|
+
the application cache for provider data.'
|
|
44
|
+
email:
|
|
45
|
+
- dev.bcostanzo@gmail.com
|
|
46
|
+
executables: []
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- CHANGELOG.md
|
|
51
|
+
- LICENSE
|
|
52
|
+
- README.md
|
|
53
|
+
- app/controllers/fundamentalista_rails/application_controller.rb
|
|
54
|
+
- app/controllers/fundamentalista_rails/companies_controller.rb
|
|
55
|
+
- app/controllers/fundamentalista_rails/comparisons_controller.rb
|
|
56
|
+
- app/controllers/fundamentalista_rails/holdings_controller.rb
|
|
57
|
+
- app/controllers/fundamentalista_rails/snapshots_controller.rb
|
|
58
|
+
- app/controllers/fundamentalista_rails/watchlists_controller.rb
|
|
59
|
+
- app/helpers/fundamentalista_rails/figures_helper.rb
|
|
60
|
+
- app/models/fundamentalista_rails/application_record.rb
|
|
61
|
+
- app/models/fundamentalista_rails/holding.rb
|
|
62
|
+
- app/models/fundamentalista_rails/snapshot.rb
|
|
63
|
+
- app/models/fundamentalista_rails/watchlist.rb
|
|
64
|
+
- app/views/fundamentalista_rails/companies/index.html.erb
|
|
65
|
+
- app/views/fundamentalista_rails/companies/show.html.erb
|
|
66
|
+
- app/views/fundamentalista_rails/comparisons/show.html.erb
|
|
67
|
+
- app/views/fundamentalista_rails/shared/_ratios.html.erb
|
|
68
|
+
- app/views/fundamentalista_rails/shared/_scores.html.erb
|
|
69
|
+
- app/views/fundamentalista_rails/shared/_series.html.erb
|
|
70
|
+
- app/views/fundamentalista_rails/shared/_valuation.html.erb
|
|
71
|
+
- app/views/fundamentalista_rails/watchlists/index.html.erb
|
|
72
|
+
- app/views/fundamentalista_rails/watchlists/show.html.erb
|
|
73
|
+
- app/views/layouts/fundamentalista_rails/application.html.erb
|
|
74
|
+
- config/routes.rb
|
|
75
|
+
- db/migrate/20260906000000_create_fundamentalista_rails_tables.rb
|
|
76
|
+
- lib/fundamentalista-rails.rb
|
|
77
|
+
- lib/fundamentalista_rails/cache.rb
|
|
78
|
+
- lib/fundamentalista_rails/engine.rb
|
|
79
|
+
- lib/fundamentalista_rails/version.rb
|
|
80
|
+
homepage: https://github.com/bruno-costanzo/fundamentalista-rails
|
|
81
|
+
licenses:
|
|
82
|
+
- MIT
|
|
83
|
+
metadata:
|
|
84
|
+
source_code_uri: https://github.com/bruno-costanzo/fundamentalista-rails
|
|
85
|
+
changelog_uri: https://github.com/bruno-costanzo/fundamentalista-rails/blob/main/CHANGELOG.md
|
|
86
|
+
bug_tracker_uri: https://github.com/bruno-costanzo/fundamentalista-rails/issues
|
|
87
|
+
rubygems_mfa_required: 'true'
|
|
88
|
+
rdoc_options: []
|
|
89
|
+
require_paths:
|
|
90
|
+
- lib
|
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: 3.1.3
|
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '0'
|
|
101
|
+
requirements: []
|
|
102
|
+
rubygems_version: 4.0.16
|
|
103
|
+
specification_version: 4
|
|
104
|
+
summary: 'Fundamental analysis pages for Rails: companies, comparisons and watchlists.'
|
|
105
|
+
test_files: []
|