├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── README.md ├── Rakefile ├── TODO.txt ├── examples ├── dump_latest_10k.rb ├── list_disclosures.rb ├── lists │ └── nasdaq-mid-to-mega-tech-symbols.txt ├── show_report.rb └── show_reports.rb ├── finmodeling.gemspec ├── lib ├── finmodeling.rb └── finmodeling │ ├── annual_report_filing.rb │ ├── array_with_stats.rb │ ├── assets_calculation.rb │ ├── assets_item.rb │ ├── assets_item_vectors.rb │ ├── balance_sheet_analyses.rb │ ├── balance_sheet_calculation.rb │ ├── calculation_summary.rb │ ├── can_cache_classifications.rb │ ├── can_cache_summaries.rb │ ├── can_choose_successive_periods.rb │ ├── can_classify_rows.rb │ ├── capm.rb │ ├── cash_change_calculation.rb │ ├── cash_change_item.rb │ ├── cash_change_item_vectors.rb │ ├── cash_change_summary_from_differences.rb │ ├── cash_flow_statement_analyses.rb │ ├── cash_flow_statement_calculation.rb │ ├── classifiers.rb │ ├── company.rb │ ├── company_filing.rb │ ├── company_filing_calculation.rb │ ├── company_filings.rb │ ├── comprehensive_income_calculation.rb │ ├── comprehensive_income_statement_calculation.rb │ ├── comprehensive_income_statement_item.rb │ ├── comprehensive_income_statement_item_vectors.rb │ ├── config.rb │ ├── debt_cost_of_capital.rb │ ├── equity_change_calculation.rb │ ├── equity_change_item.rb │ ├── equity_change_item_vectors.rb │ ├── factory.rb │ ├── fama_french_cost_of_equity.rb │ ├── float_helpers.rb │ ├── forecasted_reformulated_balance_sheet.rb │ ├── forecasted_reformulated_income_statement.rb │ ├── forecasts.rb │ ├── has_string_classifer.rb │ ├── income_statement_analyses.rb │ ├── income_statement_calculation.rb │ ├── income_statement_item.rb │ ├── income_statement_item_vectors.rb │ ├── invalid_filing_error.rb │ ├── liabs_and_equity_calculation.rb │ ├── liabs_and_equity_item.rb │ ├── liabs_and_equity_item_vectors.rb │ ├── linear_trend_forecasting_policy.rb │ ├── net_income_calculation.rb │ ├── net_income_summary_from_differences.rb │ ├── paths.rb │ ├── period_array.rb │ ├── quarterly_report_filing.rb │ ├── rate.rb │ ├── ratio.rb │ ├── reformulated_balance_sheet.rb │ ├── reformulated_cash_flow_statement.rb │ ├── reformulated_income_statement.rb │ ├── reformulated_shareholder_equity_statement.rb │ ├── reoi_valuation.rb │ ├── shareholder_equity_statement_calculation.rb │ ├── string_helpers.rb │ ├── time_series_estimator.rb │ ├── trailing_avg_forecasting_policy.rb │ ├── version.rb │ ├── weighted_avg_cost_of_capital.rb │ └── yahoo_finance_helpers.rb ├── spec ├── annual_report_filing_spec.rb ├── assets_calculation_spec.rb ├── assets_item_spec.rb ├── balance_sheet_analyses_spec.rb ├── balance_sheet_calculation_spec.rb ├── calculation_summary_spec.rb ├── can_classify_rows_spec.rb ├── cash_change_calculation_spec.rb ├── cash_change_item_spec.rb ├── cash_flow_statement_calculation_spec.rb ├── company_beta_spec.rb ├── company_filing_calculation_spec.rb ├── company_filing_spec.rb ├── company_filings_spec.rb ├── company_spec.rb ├── comprehensive_income_statement_calculation_spec.rb ├── comprehensive_income_statement_item_spec.rb ├── debt_cost_of_capital_spec.rb ├── equity_change_calculation_spec.rb ├── equity_change_item_spec.rb ├── factory_spec.rb ├── forecasts_spec.rb ├── income_statement_analyses_spec.rb ├── income_statement_calculation_spec.rb ├── income_statement_item_spec.rb ├── liabs_and_equity_calculation_spec.rb ├── liabs_and_equity_item_spec.rb ├── linear_trend_forecasting_policy_spec.rb ├── matchers │ └── custom_matchers.rb ├── mocks │ ├── calculation.rb │ ├── income_statement_analyses.rb │ └── sec_query.rb ├── net_income_calculation_spec.rb ├── period_array.rb ├── quarterly_report_filing_spec.rb ├── rate_spec.rb ├── ratio_spec.rb ├── reformulated_balance_sheet_spec.rb ├── reformulated_cash_flow_statement_spec.rb ├── reformulated_income_statement_spec.rb ├── reformulated_shareholder_equity_statement_spec.rb ├── reoi_valuation_spec.rb ├── shareholder_equity_statement_calculation_spec.rb ├── spec_helper.rb ├── string_helpers_spec.rb ├── time_series_estimator_spec.rb ├── trailing_avg_forecasting_policy_spec.rb └── weighted_avg_cost_of_capital_spec.rb └── tools ├── create_balance_sheet_training_vectors.rb ├── create_cash_change_training_vectors.rb ├── create_credit_debit_training_vectors.rb ├── create_equity_change_training_vectors.rb ├── create_income_statement_training_vectors.rb └── time_specs.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *swp 2 | .ruby-version 3 | pkg/ 4 | Gemfile.lock 5 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - '1.9.3' 4 | - '2.0.0' 5 | before_install: 6 | - cat /etc/*release* 7 | - sudo apt-get update -qq 8 | - sudo apt-get install -qq imagemagick libmagickwand-dev libgsl0-dev 9 | - gem update --system 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "xbrlware-ruby19", :git => "git://github.com/jimlindstrom/xbrlware-ruby19.git" 4 | #gem "xbrlware-ruby19", :path => "/home/lindstro/code/xbrlware-ruby19/" 5 | 6 | gem "xbrlware-extras", :git => "git://github.com/jimlindstrom/xbrlware-extras.git" 7 | #gem "xbrlware-extras", :path => "/home/lindstro/code/xbrlware-extras/" 8 | 9 | gem "nasdaq_query", :git => "git://github.com/jimlindstrom/nasdaq_query.git" 10 | 11 | # Specify your gem's dependencies in finmodeling.gemspec 12 | gemspec 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | 4 | task :default => "spec:basic" 5 | task :test => "spec:basic" # to support a legacy task that I've now renamed. 6 | 7 | namespace :spec do 8 | desc "run the specs (just the well-written ones)" 9 | task :basic do 10 | sh "if git status | grep 'modified:' | grep annual_report >/dev/null; then echo \"\n\nYou should get rid of ~/.finmodeling\"; fi" 11 | sh "rspec -c -fd -I. -Ispec --tag ~outdated spec/*spec.rb" 12 | end 13 | 14 | desc "run the specs (including ones that are badly written (depending on certain filings) and need rewritten" 15 | task :all do 16 | sh "if git status | grep 'modified:' | grep annual_report >/dev/null; then echo \"\n\nYou should get rid of ~/.finmodeling\"; fi" 17 | sh "rspec -c -fd -I. -Ispec spec/*spec.rb" 18 | end 19 | end 20 | 21 | desc "purges anything cached" 22 | task :purge_cache do 23 | sh "rm -rf ~/.finmodeling" 24 | end 25 | 26 | desc "purges anything cached, except the raw XBRL filing downloads." 27 | task :purge_cache_except_filings do 28 | sh "rm -rf ~/.finmodeling/classifiers/ ~/.finmodeling/constructors/ ~/.finmodeling/summaries/" 29 | end 30 | -------------------------------------------------------------------------------- /examples/dump_latest_10k.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'finmodeling' 4 | 5 | if ARGV.length != 1 6 | puts "usage #{__FILE__} " 7 | exit 8 | end 9 | 10 | filing_url = nil 11 | if !(ARGV[0] =~ /http/) 12 | company = FinModeling::Company.find(stock_symbol = ARGV[0]) 13 | if company.nil? 14 | puts "couldn't find company" 15 | exit 16 | elsif company.annual_reports.length == 0 17 | puts "no annual reports" 18 | exit 19 | end 20 | puts "company name: #{company.name}" 21 | 22 | filing_url = company.annual_reports.last.link 23 | puts "url: #{filing_url}" 24 | else 25 | filing_url = ARGV[0] 26 | end 27 | 28 | FinModeling::Config::disable_caching 29 | filing = FinModeling::AnnualReportFiling.download(filing_url) 30 | 31 | filing.print_presentations 32 | 33 | filing.print_calculations 34 | -------------------------------------------------------------------------------- /examples/list_disclosures.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'finmodeling' 4 | 5 | class Arguments 6 | def self.show_usage_and_exit 7 | puts "usage:" 8 | puts "\t#{__FILE__} " 9 | exit 10 | end 11 | 12 | def self.parse(args) 13 | a = { :stock_symbol => nil, :start_date => nil } 14 | 15 | self.show_usage_and_exit if args.length != 2 16 | a[:stock_symbol] = args[0] 17 | a[:start_date] = Time.parse(args[1]) 18 | 19 | return a 20 | end 21 | end 22 | 23 | args = Arguments.parse(ARGV) 24 | 25 | company = FinModeling::Company.find(args[:stock_symbol]) 26 | raise RuntimeError.new("couldn't find company") if !company 27 | puts "company name: #{company.name}" 28 | 29 | filings = FinModeling::CompanyFilings.new(company.filings_since_date(args[:start_date])) 30 | if filings.empty? 31 | puts "No filings..." 32 | exit 33 | end 34 | 35 | disclosure_periods = {} 36 | 37 | filings.each do |filing| 38 | 39 | filing.disclosures.each do |disclosure| 40 | disclosure_label = disclosure.summary(:period => disclosure.periods.last).title.gsub(/ \(.*/,'') 41 | 42 | disclosure_periods[disclosure_label] ||= [] 43 | disclosure_periods[disclosure_label] += disclosure.periods 44 | end 45 | end 46 | 47 | disclosure_periods.keys.sort.each do |disclosure_label| 48 | puts disclosure_label.to_s + ": " + disclosure_periods[disclosure_label].map{ |x| x.to_pretty_s }.sort.uniq.join(', ') 49 | end 50 | 51 | -------------------------------------------------------------------------------- /examples/lists/nasdaq-mid-to-mega-tech-symbols.txt: -------------------------------------------------------------------------------- 1 | ACCL 2 | ACIW 3 | ACOM 4 | ACXM 5 | ADBE 6 | ADP 7 | ADSK 8 | ADVS 9 | ALTR 10 | AMAT 11 | AMCC 12 | AMKR 13 | ANSS 14 | APKT 15 | ARBA 16 | ARRS 17 | ARUN 18 | ASGN 19 | ATML 20 | ATVI 21 | AWAY 22 | AZPN 23 | BBOX 24 | BCOV 25 | BIRT 26 | BLKB 27 | BMC 28 | BRCD 29 | BRCM 30 | BRKS 31 | BSFT 32 | CA 33 | CAVM 34 | CCMP 35 | CCOI 36 | CDNS 37 | CERN 38 | CEVA 39 | CMTL 40 | CNQR 41 | CPSI 42 | CPWR 43 | CREE 44 | CRUS 45 | CSCO 46 | CSGS 47 | CSOD 48 | CTCT 49 | CTSH 50 | CTXS 51 | CVLT 52 | CY 53 | CYMI 54 | DELL 55 | DGII 56 | DIOD 57 | DRIV 58 | DSGX 59 | EA 60 | EBIX 61 | EFII 62 | ELNK 63 | ELRC 64 | ENTR 65 | EPAY 66 | EPIQ 67 | EXAR 68 | EXTR 69 | FFIV 70 | FIRE 71 | FISV 72 | FNSR 73 | FSLR 74 | FTNT 75 | GCOM 76 | GOOG 77 | GRPN 78 | GTAT 79 | HITT 80 | HLIT 81 | HSII 82 | HSTM 83 | IDTI 84 | IGTE 85 | INAP 86 | INFA 87 | ININ 88 | INSP 89 | INTC 90 | INTU 91 | IPGP 92 | ISIL 93 | IXYS 94 | JCOM 95 | JDAS 96 | JDSU 97 | JIVE 98 | JKHY 99 | KELYA 100 | KFRC 101 | LAMR 102 | LECO 103 | LLTC 104 | LOGI 105 | LOGM 106 | LORL 107 | LPSN 108 | LRCX 109 | LSCC 110 | LUFK 111 | MANH 112 | MCHP 113 | MCRL 114 | MCRS 115 | MDAS 116 | MDCA 117 | MDRX 118 | MDSO 119 | MENT 120 | MFLX 121 | MGRC 122 | MIDD 123 | MIND 124 | MIPS 125 | MKTG 126 | MLNX 127 | MODL 128 | MPWR 129 | MRGE 130 | MSCC 131 | MSTR 132 | MU 133 | MXIM 134 | NATI 135 | NTAP 136 | NTCT 137 | NUAN 138 | NVDA 139 | NVLS 140 | OMCL 141 | ONNN 142 | OPNT 143 | ORCL 144 | OSIS 145 | OTEX 146 | OVTI 147 | PEGA 148 | PLAB 149 | PLXS 150 | PMCS 151 | PMTC 152 | POWI 153 | PRFT 154 | PRGS 155 | PROJ 156 | QCOM 157 | QLGC 158 | QLIK 159 | QSFT 160 | QSII 161 | RCII 162 | RFMD 163 | RMBS 164 | RNWK 165 | RP 166 | RVBD 167 | SABA 168 | SANM 169 | SAPE 170 | SATS 171 | SCSC 172 | SGI 173 | SGMS 174 | SLAB 175 | SMCI 176 | SMSC 177 | SMTC 178 | SNCR 179 | SNDK 180 | SNPS 181 | SONS 182 | SPSC 183 | SPWR 184 | SQI 185 | SSNC 186 | SSYS 187 | STEC 188 | SWKS 189 | SYKE 190 | SYMC 191 | SYNA 192 | SYNT 193 | TECD 194 | TIBX 195 | TLEO 196 | TNGO 197 | TQNT 198 | TRAK 199 | TRIP 200 | TSRA 201 | TTEC 202 | TTMI 203 | TTWO 204 | TWIN 205 | TXN 206 | TYPE 207 | TZOO 208 | UBNT 209 | ULTI 210 | UNTD 211 | UTEK 212 | VCLK 213 | VDSI 214 | VECO 215 | VIAS 216 | VLTR 217 | VRNT 218 | VRSK 219 | VRSN 220 | VRTU 221 | VSAT 222 | WWWW 223 | XLNX 224 | YHOO 225 | ZBRA 226 | ZNGA 227 | -------------------------------------------------------------------------------- /finmodeling.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/finmodeling/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Jim Lindstrom"] 6 | gem.email = ["jim.lindstrom@gmail.com"] 7 | gem.description = %q{A gem for manipulating XBRL financial filings} 8 | gem.summary = %q{A gem for manipulating XBRL financial filings} 9 | gem.homepage = "https://github.com/jimlindstrom/FinModeling" 10 | gem.license = "MIT" 11 | 12 | gem.add_dependency("fileutils") 13 | gem.add_dependency("sec_query") 14 | gem.add_dependency("edgar") 15 | 16 | gem.add_dependency("xbrlware-ruby19", "1.1.2.19.2") 17 | gem.add_dependency("xbrlware-extras", "1.1.2.19.3") 18 | gem.add_dependency("nasdaq_query") 19 | 20 | gem.add_dependency("sec_query") 21 | gem.add_dependency("naive_bayes") 22 | gem.add_dependency("statsample") 23 | gem.add_dependency("yahoofinance") 24 | gem.add_dependency("gsl") 25 | 26 | gem.add_development_dependency("rspec", "2.5") 27 | gem.add_development_dependency("rake") 28 | 29 | gem.files = `git ls-files`.split($\) 30 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 31 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 32 | gem.name = "finmodeling" 33 | gem.require_paths = ["lib"] 34 | gem.version = FinModeling::VERSION 35 | end 36 | -------------------------------------------------------------------------------- /lib/finmodeling.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'digest' 3 | 4 | require 'sec_query' 5 | require 'edgar' 6 | require 'yahoofinance' 7 | require 'finmodeling/yahoo_finance_helpers.rb' 8 | require 'nasdaq_query' 9 | 10 | require 'xbrlware-ruby19' 11 | require 'xbrlware-extras' 12 | 13 | require 'gsl' 14 | require 'naive_bayes' 15 | require 'statsample' 16 | 17 | require 'finmodeling/invalid_filing_error' 18 | 19 | require 'finmodeling/float_helpers' 20 | require 'finmodeling/string_helpers' 21 | require 'finmodeling/factory' 22 | 23 | require 'finmodeling/paths' 24 | 25 | require 'finmodeling/has_string_classifer' 26 | 27 | require 'finmodeling/period_array' 28 | require 'finmodeling/rate' 29 | require 'finmodeling/ratio' 30 | require 'finmodeling/company' 31 | 32 | require 'finmodeling/company_filings' 33 | require 'finmodeling/company_filing' 34 | require 'finmodeling/annual_report_filing' 35 | require 'finmodeling/quarterly_report_filing' 36 | 37 | require 'finmodeling/array_with_stats' 38 | require 'finmodeling/calculation_summary' 39 | 40 | require 'finmodeling/can_classify_rows' 41 | require 'finmodeling/can_cache_classifications' 42 | require 'finmodeling/can_cache_summaries' 43 | 44 | require 'finmodeling/assets_item_vectors' 45 | require 'finmodeling/assets_item' 46 | require 'finmodeling/liabs_and_equity_item_vectors' 47 | require 'finmodeling/liabs_and_equity_item' 48 | require 'finmodeling/income_statement_item_vectors' 49 | require 'finmodeling/comprehensive_income_statement_item_vectors' 50 | require 'finmodeling/income_statement_item' 51 | require 'finmodeling/comprehensive_income_statement_item' 52 | require 'finmodeling/cash_change_item_vectors' 53 | require 'finmodeling/cash_change_item' 54 | require 'finmodeling/equity_change_item_vectors' 55 | require 'finmodeling/equity_change_item' 56 | 57 | require 'finmodeling/company_filing_calculation' 58 | require 'finmodeling/can_choose_successive_periods' 59 | require 'finmodeling/balance_sheet_calculation' 60 | require 'finmodeling/assets_calculation' 61 | require 'finmodeling/liabs_and_equity_calculation' 62 | require 'finmodeling/income_statement_calculation' 63 | require 'finmodeling/comprehensive_income_statement_calculation' 64 | require 'finmodeling/net_income_calculation' 65 | require 'finmodeling/comprehensive_income_calculation' 66 | require 'finmodeling/cash_flow_statement_calculation' 67 | require 'finmodeling/cash_change_calculation' 68 | require 'finmodeling/shareholder_equity_statement_calculation' 69 | require 'finmodeling/equity_change_calculation' 70 | 71 | require 'finmodeling/net_income_summary_from_differences' 72 | require 'finmodeling/cash_change_summary_from_differences' 73 | 74 | require 'finmodeling/reformulated_income_statement' 75 | require 'finmodeling/reformulated_balance_sheet' 76 | require 'finmodeling/reformulated_cash_flow_statement' 77 | require 'finmodeling/reformulated_shareholder_equity_statement' 78 | 79 | require 'finmodeling/capm' 80 | require 'finmodeling/debt_cost_of_capital' 81 | require 'finmodeling/weighted_avg_cost_of_capital' 82 | require 'finmodeling/fama_french_cost_of_equity' 83 | require 'finmodeling/reoi_valuation' 84 | 85 | require 'finmodeling/config' 86 | 87 | require 'finmodeling/classifiers' 88 | FinModeling::Classifiers.train 89 | 90 | require 'finmodeling/balance_sheet_analyses' 91 | require 'finmodeling/income_statement_analyses' 92 | require 'finmodeling/cash_flow_statement_analyses' 93 | 94 | require 'finmodeling/forecasted_reformulated_income_statement' 95 | require 'finmodeling/forecasted_reformulated_balance_sheet' 96 | 97 | require 'finmodeling/time_series_estimator' 98 | require 'finmodeling/trailing_avg_forecasting_policy' 99 | require 'finmodeling/linear_trend_forecasting_policy' 100 | require 'finmodeling/forecasts' 101 | 102 | -------------------------------------------------------------------------------- /lib/finmodeling/array_with_stats.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | 3 | class ArrayWithStats < Array 4 | def mean 5 | return nil if empty? 6 | self.inject(:+) / self.length 7 | end 8 | 9 | def variance 10 | x_sqrd = self.map{ |x| x*x } 11 | x_sqrd_mean = (ArrayWithStats.new(x_sqrd).mean) 12 | x_sqrd_mean - (mean**2) 13 | end 14 | 15 | def linear_regression 16 | x = Array(0..(self.length-1)).to_scale 17 | y = self.to_scale 18 | Statsample::Regression.simple(x,y) 19 | end 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /lib/finmodeling/assets_calculation.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | 3 | class AssetsCalculation < CompanyFilingCalculation 4 | include CanCacheClassifications 5 | include CanCacheSummaries 6 | include CanClassifyRows 7 | 8 | BASE_FILENAME = File.join(FinModeling::BASE_PATH, "summaries/ai_") 9 | 10 | ALL_STATES = [ :oa, :fa ] 11 | NEXT_STATES = { nil => [ :oa, :fa ], 12 | :oa => [ :oa, :fa ], 13 | :fa => [ :oa, :fa ] } 14 | 15 | def summary(args) 16 | summary_cache_key = args[:period].to_pretty_s 17 | thesummary = lookup_cached_summary(summary_cache_key) 18 | return thesummary if !thesummary.nil? 19 | 20 | thesummary = super(:period => args[:period], :mapping => mapping) 21 | if !lookup_cached_classifications(BASE_FILENAME, thesummary.rows) 22 | lookahead = [4, thesummary.rows.length-1].min 23 | classify_rows(ALL_STATES, NEXT_STATES, thesummary.rows, FinModeling::AssetsItem, lookahead) 24 | save_cached_classifications(BASE_FILENAME, thesummary.rows) 25 | end 26 | 27 | save_cached_summary(summary_cache_key, thesummary) 28 | 29 | return thesummary 30 | end 31 | 32 | def mapping 33 | m = Xbrlware::ValueMapping.new 34 | m.policy[:credit] = :flip 35 | m 36 | end 37 | 38 | def has_cash_item 39 | @has_cash_item = leaf_items.any? do |leaf| 40 | leaf.name.downcase.matches_any_regex?([/cash/]) 41 | end 42 | end 43 | 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/finmodeling/assets_item.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class AssetsItem < String 3 | include HasStringClassifier 4 | 5 | BASE_FILENAME = File.join(FinModeling::BASE_PATH, "classifiers/ai_") 6 | TYPES = [ :oa, :fa ] 7 | 8 | has_string_classifier(TYPES, AssetsItem) 9 | 10 | def self.load_vectors_and_train 11 | self._load_vectors_and_train(BASE_FILENAME, FinModeling::AssetsItem::TRAINING_VECTORS) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/finmodeling/balance_sheet_analyses.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module FinModeling 4 | 5 | class BalanceSheetAnalyses < CalculationSummary 6 | def initialize(calc_summary) 7 | @title = calc_summary.title 8 | @rows = calc_summary.rows 9 | @header_row = calc_summary.header_row 10 | @key_width = calc_summary.key_width 11 | @val_width = calc_summary.val_width 12 | @max_decimals = calc_summary.max_decimals 13 | @totals_row_enabled = false 14 | end 15 | 16 | def print_regressions # FIXME: rename 17 | lr = noa_growth_row.valid_vals.linear_regression 18 | puts "\t\tNOA growth: "+ 19 | "a:#{lr.a.to_s.cap_decimals(4)}, "+ 20 | "b:#{lr.b.to_s.cap_decimals(4)}, "+ 21 | "r²:#{lr.r2.to_s.cap_decimals(4)}, "+ 22 | "σ²:#{noa_growth_row.valid_vals.variance.to_s.cap_decimals(4)}, " + 23 | ( (lr.r2 > 0.6) ? "strong fit" : ( (lr.r2 < 0.2) ? "weak fit [**]" : "avg fit") ) 24 | 25 | lr = composition_ratio_row.valid_vals.linear_regression 26 | puts "\t\tComposition ratio: "+ 27 | "a:#{lr.a.to_s.cap_decimals(4)}, "+ 28 | "b:#{lr.b.to_s.cap_decimals(4)}, "+ 29 | "r²:#{lr.r2.to_s.cap_decimals(4)}, "+ 30 | "σ²:#{composition_ratio_row.valid_vals.variance.to_s.cap_decimals(4)}, " + 31 | ( (lr.r2 > 0.6) ? "strong fit" : ( (lr.r2 < 0.2) ? "weak fit [**]" : "avg fit") ) 32 | end 33 | 34 | def noa_growth_row 35 | find_row_by_key('NOA Growth') 36 | end 37 | 38 | def composition_ratio_row 39 | find_row_by_key('Composition Ratio') 40 | end 41 | 42 | def find_row_by_key(key) # FIXME: move this to CalculationSummary 43 | self.rows.find{ |x| x.key == key } 44 | end 45 | end 46 | 47 | end 48 | 49 | -------------------------------------------------------------------------------- /lib/finmodeling/balance_sheet_calculation.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class BalanceSheetCalculation < CompanyFilingCalculation 3 | 4 | ASSETS_GOAL = "assets" 5 | ASSETS_LABELS = [ /(^total *|^consolidated *|^)assets(| BS)$/, 6 | /^assets total$/ ] 7 | ASSETS_ANTI_LABELS = [ ] 8 | ASSETS_IDS = [ /^(|Locator_|loc_)(|us-gaap_)Assets[_a-z0-9]+/ ] 9 | def assets_calculation 10 | begin 11 | @assets ||= AssetsCalculation.new(find_calculation_arc(ASSETS_GOAL, ASSETS_LABELS, ASSETS_ANTI_LABELS, ASSETS_IDS)) 12 | rescue FinModeling::InvalidFilingError => e 13 | pre_msg = "calculation tree:\n" + self.calculation.sprint_tree 14 | raise e, pre_msg+e.message, e.backtrace 15 | end 16 | end 17 | 18 | LIABS_AND_EQ_GOAL = "liabilities and equity" 19 | LIABS_AND_EQ_LABELS = [ /(^total *|^)liabilities.*and.*(equity|stockholders investment)/ ] 20 | LIABS_AND_EQ_ANTI_LABELS = [ ] 21 | LIABS_AND_EQ_IDS = [ /.*/ ] # FIXME: no checking... 22 | def liabs_and_equity_calculation 23 | begin 24 | @liabs_and_eq ||= LiabsAndEquityCalculation.new(find_calculation_arc(LIABS_AND_EQ_GOAL, LIABS_AND_EQ_LABELS, LIABS_AND_EQ_ANTI_LABELS, LIABS_AND_EQ_IDS)) 25 | rescue FinModeling::InvalidFilingError => e 26 | pre_msg = "calculation tree:\n" + self.calculation.sprint_tree 27 | raise e, pre_msg+e.message, e.backtrace 28 | end 29 | end 30 | 31 | def is_valid? 32 | puts "balance sheet's assets calculation lacks cash item" if !assets_calculation.has_cash_item 33 | puts "balance sheet's liabilities and equity calculation lacks equity item" if !liabs_and_equity_calculation.has_equity_item 34 | puts "balance sheet's isn't balanced" if !is_balanced 35 | 36 | if !assets_calculation.has_cash_item || !liabs_and_equity_calculation.has_equity_item || !is_balanced 37 | if assets_calculation 38 | puts "assets summary:" 39 | assets_calculation.summary(:period => periods.last).print 40 | end 41 | if liabs_and_equity_calculation 42 | puts "liabs & equity summary:" 43 | liabs_and_equity_calculation.summary(:period => periods.last).print 44 | end 45 | puts "calculation tree:\n" + self.calculation.sprint_tree(indent_count=0, simplified=true) 46 | end 47 | 48 | return (assets_calculation.has_cash_item && 49 | liabs_and_equity_calculation.has_equity_item && 50 | is_balanced) 51 | end 52 | 53 | def reformulated(period) 54 | return ReformulatedBalanceSheet.new(period, 55 | assets_calculation .summary(:period=>period), 56 | liabs_and_equity_calculation.summary(:period=>period)) 57 | end 58 | 59 | def write_constructor(file, item_name) 60 | item_calc_name = item_name + "_calc" 61 | @calculation.write_constructor(file, item_calc_name) 62 | file.puts "#{item_name} = FinModeling::BalanceSheetCalculation.new(#{item_calc_name})" 63 | end 64 | 65 | def is_balanced 66 | left = assets_calculation .leaf_items_sum(:period => periods.last, :mapping => assets_calculation.mapping) 67 | right = liabs_and_equity_calculation.leaf_items_sum(:period => periods.last, :mapping => liabs_and_equity_calculation.mapping) 68 | 69 | is_bal = (left - right) < ((0.5*(left + right))/1000.0) 70 | if !is_bal 71 | puts "balance sheet last period: #{periods.last.inspect}" 72 | puts "balance sheet left side: #{left}" 73 | puts "balance sheet right side: #{right}" 74 | puts "left:" 75 | assets_calculation.summary(:period => periods.last).print 76 | puts "right:" 77 | liabs_and_equity_calculation.summary(:period => periods.last).print 78 | end 79 | is_bal 80 | end 81 | 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/finmodeling/can_cache_classifications.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | 3 | module CanCacheClassifications 4 | protected 5 | 6 | def lookup_cached_classifications(base_filename, rows) 7 | filename = rows_to_filename(base_filename, rows) 8 | return false if !File.exists?(filename) || !Config.caching_enabled? 9 | 10 | f = File.open(filename, "r") 11 | rows.each do |row| 12 | row.type = f.gets.chomp.to_sym 13 | end 14 | f.close 15 | return true 16 | end 17 | 18 | def save_cached_classifications(base_filename, rows) 19 | filename = rows_to_filename(base_filename, rows) 20 | FileUtils.mkdir_p(File.dirname(filename)) if !File.exists?(File.dirname(filename)) 21 | f = File.open(filename, "w") 22 | rows.each do |row| 23 | f.puts row.type.to_s 24 | end 25 | f.close 26 | end 27 | 28 | private 29 | 30 | def rows_to_filename(base_filename, rows) 31 | unique_str = Digest::SHA1.hexdigest(rows.map{ |row| row.key }.join) 32 | filename = base_filename + unique_str + ".txt" 33 | end 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /lib/finmodeling/can_cache_summaries.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | 3 | module CanCacheSummaries 4 | protected 5 | 6 | def lookup_cached_summary(key) 7 | @summary_cache = { } if @summary_cache.nil? 8 | return @summary_cache[key] 9 | end 10 | 11 | def save_cached_summary(key, summary) 12 | @summary_cache[key] = summary 13 | end 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /lib/finmodeling/can_choose_successive_periods.rb: -------------------------------------------------------------------------------- 1 | module CanChooseSuccessivePeriods 2 | protected 3 | 4 | def choose_successive_periods(cur_calc, prev_calc) 5 | if cur_calc.periods.halfyearly .any? && prev_calc.periods.quarterly .any? 6 | return [ cur_calc.periods.halfyearly .last , prev_calc.periods.quarterly .last ] 7 | elsif cur_calc.periods.threequarterly.any? && prev_calc.periods.halfyearly .any? 8 | return [ cur_calc.periods.threequarterly.last , prev_calc.periods.halfyearly .last ] 9 | elsif cur_calc.periods.yearly .any? && prev_calc.periods.threequarterly.any? 10 | return [ cur_calc.periods.yearly .last , prev_calc.periods.threequarterly.last ] 11 | end 12 | 13 | return [ nil, nil ] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/finmodeling/can_classify_rows.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | 3 | module CanClassifyRows # simple viterbi classifier, with N-element lookahead 4 | protected 5 | 6 | def classify_rows(all_states, allowed_next_states, rows, row_item_type, lookahead) 7 | estimate_of_item_being_in_state = rows.map { |row| row_item_type.new(row.key).classification_estimates } 8 | 9 | prev_state = nil 10 | rows.each_with_index do |row, item_index| 11 | cur_lookahead = [lookahead, rows.length-item_index-1].min 12 | row.type = classify_row(all_states, allowed_next_states, 13 | estimate_of_item_being_in_state, 14 | item_index, prev_state, cur_lookahead)[:state] 15 | raise RuntimeError.new("couldn't classify....") if row.type.nil? 16 | 17 | prev_state = row.type 18 | end 19 | end 20 | 21 | private 22 | 23 | def classify_row(all_states, allowed_next_states, estimate_of_item_being_in_state, item_index, prev_state, lookahead) 24 | best_overall = { :estimate => -100000000.0, :state => nil } 25 | best_allowed = { :estimate => -100000000.0, :state => nil } 26 | 27 | all_states.each do |state| 28 | cur = { :estimate => estimate_of_item_being_in_state[item_index][state], :state => state } 29 | raise RuntimeError.new("estimate is nil: #{estimate_of_item_being_in_state[item_index]} for state #{state}") if !cur[:estimate] 30 | 31 | if lookahead > 0 32 | future_error = classify_row(all_states, allowed_next_states, 33 | estimate_of_item_being_in_state, 34 | item_index+1, state, lookahead-1)[:error] 35 | cur[:estimate] -= future_error 36 | end 37 | 38 | if cur[:estimate] > best_overall[:estimate] 39 | best_overall = cur 40 | end 41 | 42 | is_allowed = allowed_next_states[prev_state] && allowed_next_states[prev_state].include?(state) 43 | if is_allowed && (cur[:estimate] > best_allowed[:estimate]) 44 | best_allowed = cur 45 | end 46 | end 47 | 48 | return { :state => best_allowed[:state], 49 | :error => best_overall[:estimate] - 50 | best_allowed[:estimate] } 51 | end 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /lib/finmodeling/capm.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | 3 | module CAPM 4 | # References: 5 | # 1. http://business.baylor.edu/don_cunningham/How_Firms_Estimate_Cost_of_Capital_(2011).pdf 6 | # "Current Trends in Estimating and Applying the Cost of Capital" (2011) 7 | # 2. http://pages.stern.nyu.edu/~adamodar/pdfiles/valn2ed/ch8.pdf 8 | # "Estimating Risk Parameters and Costs of Financing" 9 | # 3. http://www.cb.wsu.edu/~nwalcott/finance425/Readings/BRUNEREst_Cost_of_Capital.pdf 10 | # "Best Practices in Estimating the Cost of Capital: Survey and Synthesis" (1998) 11 | # 4. http://www.nek.lu.se/NEKAVI/Cost%20of%20Capital%20slides.pdf 12 | # "Estimating Cost of Capital" (2009) 13 | 14 | MARKET_PREMIUM = 0.055 # FIXME: this is totally arbitrary. Find a better way to represent the fact that this is a probability distribution 15 | 16 | class RiskFreeRate 17 | # Possible symbols: 18 | # "^TNX" -> CBOEInterestRate10-YearT-Note (Good for long-term, future-oriented decisions) 19 | # ? -> 90-day t-bill (Good for historical short-period R_f estimation) 20 | def self.forward_estimate(risk_free_symbol="^TNX") 21 | quotes = YahooFinance::get_HistoricalQuotes_days(URI::encode(risk_free_symbol), num_days=1) 22 | FinModeling::Rate.new(quotes.last.adjClose / 100.0) 23 | end 24 | end 25 | 26 | class Beta 27 | # Possible index tickers: 28 | # "Spy" -> S&P 500 29 | # "^IXIC" -> Nasdaq 30 | def self.from_ticker(company_ticker, num_days=6*365, index_ticker="SPY") 31 | index_quotes = FamaFrench::EquityHistoricalData.new(index_ticker, num_days) 32 | company_quotes = FamaFrench::EquityHistoricalData.new(company_ticker, num_days) 33 | 34 | raise "no index returns" if !index_quotes || index_quotes.none? 35 | raise "no company returns" if !company_quotes || company_quotes.none? 36 | 37 | common_dates = index_quotes .year_and_month_strings & 38 | company_quotes.year_and_month_strings 39 | 40 | index_quotes .filter_by_date!(common_dates) 41 | company_quotes.filter_by_date!(common_dates) 42 | 43 | raise "no index returns (after filtering)" if !index_quotes || index_quotes.none? 44 | raise "no company returns (after filtering)" if !company_quotes || company_quotes.none? 45 | 46 | index_div_hist = NasdaqQuery::DividendHistory.for_symbol(index_ticker) 47 | company_div_hist = NasdaqQuery::DividendHistory.for_symbol(company_ticker) 48 | 49 | index_monthly_returns = index_quotes .monthly_returns(index_div_hist) 50 | company_monthly_returns = company_quotes.monthly_returns(company_div_hist) 51 | 52 | raise "no monthly index returns" if !index_monthly_returns || index_monthly_returns.none? 53 | raise "no monthly company returns" if !company_monthly_returns || company_monthly_returns.none? 54 | 55 | x = GSL::Vector.alloc(index_monthly_returns) 56 | y = GSL::Vector.alloc(company_monthly_returns) 57 | intercept, slope, cov00, cov01, cov11, chisq, status = GSL::Fit::linear(x, y) 58 | 59 | # FIXME: evaluate [intercept - Rf*(1-beta)]. It tells how much better/worse than expected (given its risk) the stock did. [per time period] 60 | 61 | # FIXME: subtracting/adding one standard error of the beta gives a 95% confidence interval. That could be used to give a confidence interval 62 | # for the resulting valuation. 63 | 64 | beta = slope 65 | end 66 | end 67 | 68 | class AdjustedBeta # see: http://financetrain.com/adjusted-and-unadjusted-beta/ 69 | MEAN_LONG_TERM_BETA = 1.0 70 | def self.from_beta(raw_beta) 71 | ((2.0*raw_beta) + (1.0*MEAN_LONG_TERM_BETA)) / 3.0 72 | end 73 | end 74 | 75 | class EquityCostOfCapital 76 | def self.from_beta(beta) 77 | Rate.new(RiskFreeRate.forward_estimate.value + (beta * MARKET_PREMIUM)) 78 | end 79 | 80 | def self.from_ticker(company_ticker) 81 | raw_beta = Beta.from_ticker(company_ticker) 82 | puts "CAPM::EquityCostOfCapital -> raw beta = #{raw_beta}" 83 | adj_beta = AdjustedBeta.from_beta(raw_beta) 84 | puts "CAPM::EquityCostOfCapital -> adj beta = #{adj_beta}" 85 | self.from_beta(adj_beta) 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/finmodeling/cash_change_calculation.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | 3 | class CashChangeCalculation < CompanyFilingCalculation 4 | include CanCacheClassifications 5 | include CanCacheSummaries 6 | include CanClassifyRows 7 | 8 | BASE_FILENAME = File.join(FinModeling::BASE_PATH, "summaries/cash_") 9 | 10 | ALL_STATES = [ :c, :i, :d, :f ] 11 | NEXT_STATES = { nil => [ :c ], 12 | :c => [ :c, :i, :d, :f ], 13 | :i => [ :i, :d, :f ], 14 | :d => [ :i, :d, :f ], 15 | :f => [ :d, :f ] } 16 | 17 | def summary(args) 18 | summary_cache_key = args[:period].to_pretty_s 19 | summary = lookup_cached_summary(summary_cache_key) 20 | return summary if !summary.nil? && false # FIXME: get rid of "and false" 21 | 22 | mapping = Xbrlware::ValueMapping.new 23 | mapping.policy[:unknown] = :flip 24 | mapping.policy[:credit] = :flip 25 | mapping.policy[:debit] = :no_action 26 | mapping.policy[:netincome] = :no_action 27 | mapping.policy[:taxes] = :no_action 28 | mapping.policy[:proceedsfromdebt] = :no_action 29 | 30 | find_and_tag_special_items(args) 31 | 32 | summary = super(:period => args[:period], :mapping => mapping) 33 | if !lookup_cached_classifications(BASE_FILENAME, summary.rows) || true # FIXME: get rid of "or true" 34 | lookahead = [4, summary.rows.length-1].min 35 | classify_rows(ALL_STATES, NEXT_STATES, summary.rows, FinModeling::CashChangeItem, lookahead) 36 | save_cached_classifications(BASE_FILENAME, summary.rows) 37 | end 38 | 39 | save_cached_summary(summary_cache_key, summary) 40 | 41 | return summary 42 | end 43 | 44 | private 45 | 46 | def find_and_tag_special_items(args) 47 | leaf_items(:period => args[:period]).each do |item| 48 | if item.name.matches_any_regex?([ /NetIncomeLoss/, 49 | /ProfitLoss/ ]) 50 | item.def = {} if !item.def 51 | item.def["xbrli:balance"] = "netincome" 52 | end 53 | 54 | if item.name =~ /IncreaseDecreaseInIncomeTaxes/ 55 | item.def = {} if !item.def 56 | item.def["xbrli:balance"] = "taxes" 57 | end 58 | 59 | if item.name =~ /ProceedsFromDebtNetOfIssuanceCosts/ 60 | item.def = {} if !item.def 61 | item.def["xbrli:balance"] = "proceedsfromdebt" 62 | end 63 | end 64 | end 65 | 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/finmodeling/cash_change_item.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class CashChangeItem < String 3 | include HasStringClassifier 4 | 5 | BASE_FILENAME = File.join(FinModeling::BASE_PATH, "classifiers/cci_") 6 | TYPES = [ :c, :i, :d, :f ] 7 | 8 | has_string_classifier(TYPES, CashChangeItem) 9 | 10 | def self.load_vectors_and_train 11 | self._load_vectors_and_train(BASE_FILENAME, FinModeling::CashChangeItem::TRAINING_VECTORS) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/finmodeling/cash_change_summary_from_differences.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class CashChangeSummaryFromDifferences 3 | def initialize(re_cfs1, re_cfs2) 4 | @re_cfs1 = re_cfs1 5 | @re_cfs2 = re_cfs2 6 | end 7 | def filter_by_type(key) 8 | case key 9 | when :c 10 | @cs = FinModeling::CalculationSummary.new 11 | @cs.title = "Cash from Operations" 12 | @cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @re_cfs1.cash_from_operations.total] ), 13 | CalculationRow.new(:key => "Second Row", :vals => [-@re_cfs2.cash_from_operations.total] ) ] 14 | return @cs 15 | when :i 16 | @cs = FinModeling::CalculationSummary.new 17 | @cs.title = "Cash Investments in Operations" 18 | @cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @re_cfs1.cash_investments_in_operations.total] ), 19 | CalculationRow.new(:key => "Second Row", :vals => [-@re_cfs2.cash_investments_in_operations.total] ) ] 20 | return @cs 21 | when :d 22 | @cs = FinModeling::CalculationSummary.new 23 | @cs.title = "Payments to Debtholders" 24 | @cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @re_cfs1.payments_to_debtholders.total] ), 25 | CalculationRow.new(:key => "Second Row", :vals => [-@re_cfs2.payments_to_debtholders.total] ) ] 26 | return @cs 27 | when :f 28 | @cs = FinModeling::CalculationSummary.new 29 | @cs.title = "Payments to Stockholders" 30 | @cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @re_cfs1.payments_to_stockholders.total] ), 31 | CalculationRow.new(:key => "Second Row", :vals => [-@re_cfs2.payments_to_stockholders.total] ) ] 32 | return @cs 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/finmodeling/cash_flow_statement_analyses.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module FinModeling 4 | 5 | class CashFlowStatementAnalyses < CalculationSummary 6 | def initialize(calc_summary) 7 | @title = calc_summary.title 8 | @rows = calc_summary.rows 9 | @header_row = calc_summary.header_row 10 | @key_width = calc_summary.key_width 11 | @val_width = calc_summary.val_width 12 | @max_decimals = calc_summary.max_decimals 13 | @totals_row_enabled = false 14 | end 15 | 16 | def print_regressions # FIXME: rename 17 | lr = ni_over_c_row.valid_vals.linear_regression 18 | puts "\t\tNI / C: "+ 19 | "a:#{lr.a.to_s.cap_decimals(4)}, "+ 20 | "b:#{lr.b.to_s.cap_decimals(4)}, "+ 21 | "r²:#{lr.r2.to_s.cap_decimals(4)}, "+ 22 | "σ²:#{ni_over_c_row.valid_vals.variance.to_s.cap_decimals(4)}, " + 23 | ( (lr.r2 > 0.6) ? "strong fit" : ( (lr.r2 < 0.2) ? "weak fit [**]" : "avg fit") ) 24 | end 25 | 26 | def ni_over_c_row 27 | find_row_by_key('NI / C') 28 | end 29 | 30 | def find_row_by_key(key) # FIXME: move this to CalculationSummary 31 | self.rows.find{ |x| x.key == key } 32 | end 33 | end 34 | 35 | end 36 | 37 | -------------------------------------------------------------------------------- /lib/finmodeling/cash_flow_statement_calculation.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class CashFlowStatementCalculation < CompanyFilingCalculation 3 | include CanChooseSuccessivePeriods 4 | 5 | CASH_GOAL = "cash change" 6 | CASH_LABELS = [ /^cash and cash equivalents period increase decrease/, 7 | /^(|net )(change|increase|decrease|decrease *increase|increase *decrease) in cash and(| cash) equivalents/, 8 | /^net cash provided by used in (|operating activities )continuing operations/, 9 | /^net cash provided by used in operating activities/] 10 | CASH_ANTI_LABELS = [ ] 11 | CASH_IDS = [ /^(|Locator_|loc_)(|us-gaap_)CashAndCashEquivalentsPeriodIncreaseDecrease[_a-z0-9]+/, 12 | /^(|Locator_|loc_)(|us-gaap_)NetCashProvidedByUsedIn(|OperatingActivities)ContinuingOperations[_a-z0-9]+/ ] 13 | 14 | def cash_change_calculation 15 | @cash_change ||= CashChangeCalculation.new(find_calculation_arc(CASH_GOAL, CASH_LABELS, CASH_ANTI_LABELS, CASH_IDS)) 16 | end 17 | 18 | def is_valid? 19 | re_cfs = reformulated(periods.last) 20 | flows_are_balanced = (re_cfs.free_cash_flow.total == (-1*re_cfs.financing_flows.total)) 21 | puts "flows are not balanced" if !flows_are_balanced 22 | none_are_zero = (re_cfs.cash_from_operations.total != 0) && 23 | (re_cfs.cash_investments_in_operations.total != 0) && 24 | (re_cfs.payments_to_debtholders.total != 0) #&& 25 | #(re_cfs.payments_to_stockholders.total != 0) # I relaxed this constraint. Seems it is often legitimately zero 26 | puts "(re_cfs.cash_from_operations.total == 0)" if (re_cfs.cash_from_operations.total == 0) 27 | puts "(re_cfs.cash_investments_in_operations.total == 0)" if (re_cfs.cash_investments_in_operations.total == 0) 28 | puts "(re_cfs.payments_to_debtholders.total == 0)" if (re_cfs.payments_to_debtholders.total == 0) 29 | #puts "(re_cfs.payments_to_stockholders.total == 0)" if (re_cfs.payments_to_stockholders.total == 0) 30 | return (flows_are_balanced && none_are_zero) 31 | end 32 | 33 | def reformulated(period) 34 | return ReformulatedCashFlowStatement.new(period, cash_change_calculation.summary(:period => period)) 35 | end 36 | 37 | def latest_quarterly_reformulated(prev_cfs) 38 | if cash_change_calculation.periods.quarterly.any? 39 | period = cash_change_calculation.periods.quarterly.last 40 | lqr = reformulated(period) 41 | return lqr if lqr.flows_are_plausible? 42 | end 43 | 44 | return nil if !prev_cfs 45 | 46 | cur_period, prev_period = choose_successive_periods(cash_change_calculation, prev_cfs.cash_change_calculation) 47 | if cur_period && prev_period 48 | return reformulated(cur_period) - prev_cfs.reformulated(prev_period) 49 | end 50 | 51 | return nil 52 | end 53 | 54 | def write_constructor(file, item_name) 55 | item_calc_name = item_name + "_calc" 56 | @calculation.write_constructor(file, item_calc_name) 57 | file.puts "#{item_name} = FinModeling::CashFlowStatementCalculation.new(#{item_calc_name})" 58 | end 59 | 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/finmodeling/classifiers.rb: -------------------------------------------------------------------------------- 1 | 2 | module FinModeling 3 | class Classifiers 4 | def self.train 5 | FinModeling::AssetsItem.load_vectors_and_train 6 | FinModeling::LiabsAndEquityItem.load_vectors_and_train 7 | FinModeling::IncomeStatementItem.load_vectors_and_train 8 | FinModeling::ComprehensiveIncomeStatementItem.load_vectors_and_train 9 | FinModeling::CashChangeItem.load_vectors_and_train 10 | FinModeling::EquityChangeItem.load_vectors_and_train 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/finmodeling/company.rb: -------------------------------------------------------------------------------- 1 | module SecQuery 2 | class Filing 3 | def write_constructor(file, item) 4 | file.puts "filing = { :cik => \"#{@cik}\", :title => \"#{@title}\", :summary => \"#{@summary}\", " + 5 | ":link => \"#{@link.gsub(/"/, "\\\"")}\", :term => \"#{@term}\", :date => \"#{@date}\", :file_id => \"#{@file_id}\" }" 6 | file.puts "#{item} = SecQuery::Filing.new(filing)" 7 | end 8 | end 9 | 10 | class Entity 11 | def write_constructor(filename) 12 | FileUtils.mkdir_p(File.dirname(filename)) if !File.exists?(File.dirname(filename)) 13 | file = File.open(filename, "w") 14 | filing_names = [] 15 | @filings.select{ |x| x.title =~ /^10-/ }.each_with_index do |filing, index| 16 | filing_name = "item_#{index}" 17 | filing.write_constructor(file, filing_name) 18 | filing_names.push filing_name 19 | end 20 | file.puts "@entity = SecQuery::Entity.new({ :name => \"#{@name.gsub(/"/, "\\\"")}\", :filings => [#{filing_names.join(',')}] })" 21 | file.close 22 | end 23 | 24 | def self.load(filename) 25 | return nil if !File.exists?(filename) || !FinModeling::Config.caching_enabled? 26 | eval(File.read(filename)) 27 | return @entity 28 | end 29 | end 30 | end 31 | 32 | module FinModeling 33 | class Company 34 | def initialize(entity) 35 | @entity = entity 36 | end 37 | 38 | BASE_FILENAME = File.join(FinModeling::BASE_PATH, "companies/") 39 | def self.find(stock_symbol) 40 | filename = BASE_FILENAME + stock_symbol.upcase + ".rb" 41 | entity = SecQuery::Entity.load(filename) 42 | return Company.new(entity) if !entity.nil? 43 | begin 44 | entity = SecQuery::Entity.find(stock_symbol, { :relationships => false, 45 | :transactions => false, 46 | :filings => true }) 47 | #:filings => {:start=> 0, :count=>20, :limit=> 20} }) 48 | return nil if !entity 49 | entity.write_constructor(filename) 50 | return Company.new(entity) 51 | rescue Exception => e 52 | puts "Warning: failed to load entity" 53 | puts "\t" + e.message 54 | puts "\t" + e.backtrace.inspect.gsub(/, /, "\n\t ") 55 | return nil 56 | end 57 | end 58 | 59 | def name 60 | @entity.name.gsub(/ \(.*/, '') 61 | end 62 | 63 | def annual_reports 64 | CompanyFilings.new(sorted_reports_of_type("10-K")) 65 | end 66 | 67 | def quarterly_reports 68 | CompanyFilings.new(sorted_reports_of_type("10-Q")) 69 | end 70 | 71 | def filings_since_date(start_date) 72 | reports = self.annual_reports 73 | reports += self.quarterly_reports 74 | reports.select!{ |report| Time.parse(report.date) >= start_date } 75 | reports.sort!{ |x, y| Time.parse(x.date) <=> Time.parse(y.date) } 76 | 77 | filings = [] 78 | reports.each do |report| 79 | begin 80 | case report.term 81 | when "10-Q" then filings << FinModeling::QuarterlyReportFiling.download(report.link) 82 | when "10-K" then filings << FinModeling::AnnualReportFiling.download( report.link) 83 | end 84 | rescue Exception => e 85 | # *ReportFiling.download() will throw errors if it doesn't contain xbrl data. 86 | puts "Caught error in FinModeling::(.*)ReportFiling.download:" 87 | puts "\t" + e.message 88 | puts "\t" + e.backtrace.inspect.gsub(/, /, "\n\t ") 89 | end 90 | end 91 | 92 | return CompanyFilings.new(filings) 93 | end 94 | 95 | private 96 | 97 | def sorted_reports_of_type(report_type) 98 | @entity.filings.select{ |x| x.term == report_type }.sort{ |x,y| x.date <=> y.date } 99 | end 100 | 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/finmodeling/company_filing.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | 3 | class CachedAnnualFiling 4 | attr_accessor :balance_sheet, :income_statement, :comprehensive_income_statement, :cash_flow_statement, :shareholder_equity_statement, :disclosures 5 | def initialize(bs, is, cis, cfs, ses, disclosures) 6 | @balance_sheet = bs 7 | @income_statement = is 8 | @comprehensive_income_statement = cis 9 | @cash_flow_statement = cfs 10 | @shareholder_equity_statement = ses 11 | @disclosures = disclosures 12 | end 13 | 14 | def has_an_income_statement? 15 | !@income_statement.nil? 16 | end 17 | 18 | def has_a_comprehensive_income_statement? 19 | !@comprehensive_income_statement.nil? 20 | end 21 | 22 | def has_a_shareholder_equity_statement? 23 | !@shareholder_equity_statement.nil? 24 | end 25 | 26 | def is_valid? 27 | puts "balance sheet is not valid" if !@balance_sheet.is_valid? 28 | puts "income statment is not valid" if has_an_income_statement? && !@income_statement.is_valid? 29 | puts "comprehensive income statment is not valid" if has_a_comprehensive_income_statement? && !@comprehensive_income_statement.is_valid? 30 | #puts "cash flow statement is not valid" if !cash_flow_statement.is_valid? 31 | 32 | return false if !@balance_sheet.is_valid? 33 | return false if has_an_income_statement? && !@income_statement.is_valid? 34 | return false if has_a_comprehensive_income_statement? && !@comprehensive_income_statement.is_valid? 35 | #return false if !@cash_flow_statement.is_valid? # FIXME: why can't we enable this? 36 | return true 37 | end 38 | end 39 | 40 | class CachedQuarterlyFiling < CachedAnnualFiling 41 | end 42 | 43 | class CompanyFiling 44 | DOWNLOAD_PATH = File.join(FinModeling::BASE_PATH, "filings/") 45 | attr_accessor :instance # FIXME: hide this 46 | 47 | def initialize(download_dir) 48 | instance_file = Xbrlware.file_grep(download_dir)["ins"] 49 | if instance_file.nil? 50 | raise "Filing (\"#{download_dir}\") has no instance files. No XBRL filing?" 51 | end 52 | 53 | @instance = Xbrlware.ins(instance_file) 54 | @taxonomy = @instance.taxonomy 55 | @taxonomy.init_all_lb 56 | end 57 | 58 | def self.download(url) 59 | FileUtils.mkdir_p(DOWNLOAD_PATH) if !File.exists?(DOWNLOAD_PATH) 60 | download_dir = DOWNLOAD_PATH + url.split("/")[-2] 61 | if !File.exists?(download_dir) 62 | dl = Xbrlware::Edgar::HTMLFeedDownloader.new() 63 | dl.download(url, download_dir) 64 | end 65 | 66 | return self.new(download_dir) 67 | end 68 | 69 | def print_presentations 70 | presentations = @taxonomy.prelb.presentation 71 | presentations.each { |pres| pres.print_tree } 72 | end 73 | 74 | def print_calculations 75 | calculations=@taxonomy.callb.calculation 76 | calculations.each { |calc| calc.print_tree } 77 | end 78 | 79 | def disclosures 80 | @taxonomy.callb 81 | .calculation 82 | .select{ |x| x.is_disclosure? } 83 | .map{ |x| CompanyFilingCalculation.new(x) } 84 | end 85 | 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/finmodeling/company_filing_calculation.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class CompanyFilingCalculation 3 | attr_accessor :calculation # FIXME: get rid of this (it was just to enable testing) 4 | 5 | def initialize(calculation) 6 | @calculation = calculation 7 | end 8 | 9 | def label 10 | @calculation.label 11 | end 12 | 13 | def periods 14 | arr = leaf_items.map{ |x| x.context.period } 15 | .sort{ |x,y| x.to_pretty_s <=> y.to_pretty_s } 16 | .uniq 17 | PeriodArray.new(arr) 18 | end 19 | 20 | def leaf_items(args={}) 21 | @calculation.leaf_items(args[:period]) 22 | end 23 | 24 | def leaf_items_sum(args) 25 | leaves = leaf_items(:period => args[:period]) 26 | values = leaves.map{ |item| item.value(args[:mapping]) } 27 | values.inject(:+) 28 | end 29 | 30 | def summary(args) 31 | calc_summary = CalculationSummary.new 32 | calc_summary.title = case 33 | when @calculation.instance_variable_defined?(:@title) then @calculation.title 34 | when @calculation.instance_variable_defined?(:@label) then @calculation.label 35 | else "[No title]" 36 | end 37 | calc_summary.title += case 38 | when @calculation.instance_variable_defined?(:@item_id) then " (#{@calculation.item_id})" 39 | when @calculation.instance_variable_defined?(:@role) then " (#{@calculation.role })" 40 | else "" 41 | end 42 | 43 | calc_summary.rows = leaf_items(args).collect do |item| 44 | CalculationRow.new(:key => item.pretty_name, 45 | :vals => [ item.value(args[:mapping] )]) 46 | end 47 | 48 | return calc_summary 49 | end 50 | 51 | def write_constructor(file, item_name) 52 | item_calc_name = item_name + "_calc" 53 | @calculation.write_constructor(file, item_calc_name) 54 | file.puts "#{item_name} = FinModeling::CompanyFilingCalculation.new(#{item_calc_name})" 55 | end 56 | 57 | protected 58 | 59 | def find_calculation_arc(friendly_goal, label_regexes, anti_label_regexes, id_regexes, criterion=:first) 60 | calcs = @calculation.arcs.select{ |x| x.label.downcase.gsub(/[^a-z ]/, '').matches_any_regex?(label_regexes) && 61 | !x.label.downcase.gsub(/[^a-z ]/, '').matches_any_regex?(anti_label_regexes) } 62 | 63 | if calcs.empty? 64 | summary_of_arcs = @calculation.arcs.map{ |x| "\t\"#{x.label}\"" }.join("\n") 65 | raise InvalidFilingError.new("Couldn't find #{friendly_goal} in:\n" + summary_of_arcs + "\nTried: #{label_regexes.inspect}. (Ignoring: #{anti_label_regexes.inspect}.).") 66 | end 67 | 68 | calc = case 69 | when criterion == :first 70 | calcs.first 71 | when criterion == :longest 72 | calcs.sort{ |x,y| x.leaf_items(periods.last).length <=> y.leaf_items(periods.last).length }.last 73 | else 74 | raise ArgumentError.new("\"#{criterion}\" is not a valid criterion") 75 | end 76 | 77 | if !calc.item_id.matches_any_regex?(id_regexes) 78 | puts "Warning: #{friendly_goal} id is not recognized: #{calc.item_id}" 79 | end 80 | 81 | return calc 82 | end 83 | 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/finmodeling/comprehensive_income_calculation.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class ComprehensiveIncomeCalculation < CompanyFilingCalculation 3 | include CanCacheClassifications 4 | include CanCacheSummaries 5 | include CanClassifyRows 6 | 7 | BASE_FILENAME = File.join(FinModeling::BASE_PATH, "summaries/comprehensive_income_") 8 | 9 | ALL_STATES = [ :or, :cogs, :oe, :oibt, :fibt, :tax, :ooiat, :fiat, :ni, :ooci, :ooci_nci, :foci, :unkoci ] 10 | NEXT_STATES = { nil => [ :or, :ni ], 11 | :or => [ :or, :cogs, :oe, :oibt, :fibt ], 12 | :cogs => [ :cogs, :oe, :oibt, :fibt, :tax ], 13 | :oe => [ :oe, :oibt, :fibt, :tax ], 14 | :oibt => [ :oibt, :fibt, :tax ], # obit/fibt can cycle back/forth 15 | :fibt => [ :obit, :fibt, :tax ], # obit/fibt can cycle back/forth 16 | :tax => [ :ooiat, :fiat, :ooci, :ooci_nci, :foci, :unkoci ], # 1 tax item. then moves forward. 17 | :ooiat => [ :ooiat, :fiat, :ooci, :ooci_nci, :foci, :unkoci ], # ooiat/fiat can cycle back/forth 18 | :fiat => [ :ooiat, :fiat, :ooci, :ooci_nci, :foci, :unkoci ], # ooiat/fiat can cycle back/forth 19 | 20 | :ni => [ :ooci, :ooci_nci, :foci, :unkoci ], # after ni, no ordering 21 | 22 | :ooci => [ :ooci, :ooci_nci, :foci, :unkoci ], # after ni, no ordering 23 | :ooci_nci => [ :ooci, :ooci_nci, :foci, :unkoci ], # after ni, no ordering 24 | :foci => [ :ooci, :ooci_nci, :foci, :unkoci ], # after ni, no ordering 25 | :unkoci => [ :ooci, :ooci_nci, :foci, :unkoci ] }# after ni, no ordering 26 | 27 | def summary(args) 28 | summary_cache_key = args[:period].to_pretty_s 29 | thesummary = lookup_cached_summary(summary_cache_key) 30 | return thesummary if !thesummary.nil? 31 | 32 | mapping = Xbrlware::ValueMapping.new 33 | mapping.policy[:debit] = :flip 34 | 35 | thesummary = super(:period => args[:period], :mapping => mapping) 36 | if !lookup_cached_classifications(BASE_FILENAME, thesummary.rows) 37 | lookahead = [4, thesummary.rows.length-1].min 38 | classify_rows(ALL_STATES, NEXT_STATES, thesummary.rows, FinModeling::ComprehensiveIncomeStatementItem, lookahead) 39 | save_cached_classifications(BASE_FILENAME, thesummary.rows) 40 | end 41 | 42 | save_cached_summary(summary_cache_key, thesummary) 43 | 44 | return thesummary 45 | end 46 | 47 | def has_revenue_item? 48 | @has_revenue_item ||= summary(:period => periods.last).rows.any? do |row| 49 | row.type == :or 50 | end 51 | end 52 | 53 | def has_net_income_item? 54 | @has_net_income_item ||= summary(:period => periods.last).rows.any? do |row| 55 | row.type == :ni 56 | end 57 | end 58 | 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/finmodeling/comprehensive_income_statement_calculation.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class ComprehensiveIncomeStatementCalculation < CompanyFilingCalculation 3 | include CanChooseSuccessivePeriods 4 | 5 | CI_GOAL = "comprehensive income" 6 | CI_LABELS = [ /^comprehensive (income|loss|loss income|income loss)(| net of tax)(| attributable to .*)$/ ] 7 | CI_ANTI_LABELS = [ /noncontrolling interest/, 8 | /minority interest/ ] 9 | CI_IDS = [ /^(|Locator_|loc_)(|us-gaap_)ComprehensiveIncomeNetOfTax[_0-9a-z]+/ ] 10 | def comprehensive_income_calculation 11 | begin 12 | @ci ||= ComprehensiveIncomeCalculation.new(find_calculation_arc(CI_GOAL, CI_LABELS, CI_ANTI_LABELS, CI_IDS)) 13 | rescue FinModeling::InvalidFilingError => e 14 | pre_msg = "calculation tree:\n" + self.calculation.sprint_tree 15 | raise e, pre_msg+e.message, e.backtrace 16 | end 17 | end 18 | 19 | def is_valid? 20 | if !comprehensive_income_calculation.has_net_income_item? && !comprehensive_income_calculation.has_revenue_item? 21 | puts "comprehensive income statement's comprehensive income calculation lacks net income item" 22 | puts "comprehensive income statement's comprehensive income calculation lacks sales/revenue item" 23 | if comprehensive_income_calculation 24 | puts "summary:" 25 | comprehensive_income_calculation.summary(:period => periods.last).print 26 | end 27 | puts "calculation tree:\n" + self.calculation.sprint_tree(indent_count=0, simplified=true) 28 | end 29 | return (comprehensive_income_calculation.has_revenue_item? || comprehensive_income_calculation.has_net_income_item?) 30 | end 31 | 32 | def reformulated(period, dummy_comprehensive_income_calculation) # 2nd param is just to keep signature consistent w/ IncomeStatement::reformulated 33 | # The way ReformulatedIncomeStatement.new() is implemented, it'll just ignore rows with types it 34 | # doesn't know about (like OCI). So this should extract just the NI-related rows. 35 | return ReformulatedIncomeStatement.new(period, 36 | comprehensive_income_calculation.summary(:period=>period), # NI 37 | comprehensive_income_calculation.summary(:period=>period)) # CI 38 | end 39 | 40 | def latest_quarterly_reformulated(dummy_cur_ci_calc, prev_stmt, prev_ci_calc) 41 | if comprehensive_income_calculation.periods.quarterly.any? 42 | period = comprehensive_income_calculation.periods.quarterly.last 43 | lqr = reformulated(period, comprehensive_income_calculation) 44 | 45 | if (lqr.operating_revenues.total.abs > 1.0) && # FIXME: make an is_valid here? 46 | (lqr.cost_of_revenues .total.abs > 1.0) # FIXME: make an is_valid here? 47 | return lqr 48 | end 49 | end 50 | 51 | return nil if !prev_stmt 52 | 53 | prev_calc = prev_stmt.respond_to?(:net_income_calculation) ? prev_stmt.net_income_calculation : prev_stmt.comprehensive_income_calculation 54 | 55 | cur_period, prev_period = choose_successive_periods(comprehensive_income_calculation, prev_calc) 56 | if cur_period && prev_period 57 | new_re_is = reformulated(cur_period, comprehensive_income_calculation) - prev_stmt.reformulated(prev_period, prev_ci_calc) 58 | # the above subtraction doesn't know what period you want. So let's patch the result to have 59 | # a quarterly period with the right end-points 60 | new_re_is.period = Xbrlware::Context::Period.new({"start_date"=>prev_period.value["end_date"], 61 | "end_date" =>cur_period.value["end_date"]}) 62 | return new_re_is 63 | end 64 | 65 | return nil 66 | end 67 | 68 | def write_constructor(file, item_name) 69 | item_calc_name = item_name + "_calc" 70 | @calculation.write_constructor(file, item_calc_name) 71 | file.puts "#{item_name} = FinModeling::ComprehensiveIncomeStatementCalculation.new(#{item_calc_name})" 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/finmodeling/comprehensive_income_statement_item.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class ComprehensiveIncomeStatementItem < String 3 | include HasStringClassifier 4 | 5 | BASE_FILENAME = File.join(FinModeling::BASE_PATH, "classifiers/cisi_") 6 | TYPES = [ :or, :cogs, :oe, :oibt, :fibt, :tax, :ooiat, :fiat, :ni, :ooci, :ooci_nci, :foci, :unkoci ] 7 | # same as in IncomeStatementItem, plus four new types: 8 | # ni (net income -- optional, for when it is rolled up, versus (more typically) presented in the same detail as in the income statement) 9 | # ooci (operating other comperhensive income) 10 | # ooci_nci (operating other comperhensive income - non-controling interest) 11 | # foci (financial other comperhensive income) 12 | # unkoci (unknown (either operating or financial) other comperhensive income) 13 | 14 | has_string_classifier(TYPES, ComprehensiveIncomeStatementItem) 15 | 16 | def self.load_vectors_and_train 17 | self._load_vectors_and_train(BASE_FILENAME, FinModeling::ComprehensiveIncomeStatementItem::TRAINING_VECTORS) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/finmodeling/config.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class Config 3 | @@caching_enabled = true 4 | def self.enable_caching 5 | @@caching_enabled = true 6 | end 7 | def self.disable_caching 8 | @@caching_enabled = false 9 | end 10 | def self.caching_enabled? 11 | @@caching_enabled 12 | end 13 | 14 | @@balance_detail_enabled = false 15 | def self.enable_balance_detail 16 | @@balance_detail_enabled = true 17 | end 18 | def self.disable_balance_detail 19 | @@balance_detail_enabled = false 20 | end 21 | def self.balance_detail_enabled? 22 | @@balance_detail_enabled 23 | end 24 | 25 | @@income_detail_enabled = false 26 | def self.enable_income_detail 27 | @@income_detail_enabled = true 28 | end 29 | def self.disable_income_detail 30 | @@income_detail_enabled = false 31 | end 32 | def self.income_detail_enabled? 33 | @@income_detail_enabled 34 | end 35 | end 36 | end 37 | 38 | -------------------------------------------------------------------------------- /lib/finmodeling/debt_cost_of_capital.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class DebtCostOfCapital 3 | def self.calculate(opts) 4 | case 5 | when opts[:after_tax_cost] && !opts[:before_tax_cost] && !opts[:marginal_tax_rate] 6 | Rate.new(opts[:after_tax_cost].value) 7 | when !opts[:after_tax_cost] && opts[:before_tax_cost] && opts[:marginal_tax_rate] 8 | Rate.new(opts[:before_tax_cost].value * (1.0 - (opts[:marginal_tax_rate].value))) 9 | else 10 | raise ArgumentError 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/finmodeling/equity_change_calculation.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | 3 | class EquityChangeCalculation < CompanyFilingCalculation 4 | include CanCacheClassifications 5 | include CanCacheSummaries 6 | include CanClassifyRows 7 | 8 | BASE_FILENAME = File.join(FinModeling::BASE_PATH, "summaries/equity_change_") 9 | 10 | ALL_STATES = [ :share_issue, :share_repurch, :minority_int, :common_div, :net_income, :oci, :preferred_div ] 11 | NEXT_STATES = { nil => [ :share_issue, :share_repurch, :minority_int, :common_div, :net_income, :oci, :preferred_div ], 12 | :share_issue => [ :share_issue, :share_repurch, :minority_int, :common_div, :net_income, :oci, :preferred_div ], 13 | :share_repurch => [ :share_issue, :share_repurch, :minority_int, :common_div, :net_income, :oci, :preferred_div ], 14 | :minority_int => [ :share_issue, :share_repurch, :minority_int, :common_div, :net_income, :oci, :preferred_div ], 15 | :common_div => [ :share_issue, :share_repurch, :minority_int, :common_div, :net_income, :oci, :preferred_div ], 16 | :net_income => [ :share_issue, :share_repurch, :minority_int, :common_div, :net_income, :oci, :preferred_div ], 17 | :oci => [ :share_issue, :share_repurch, :minority_int, :common_div, :net_income, :oci, :preferred_div ], 18 | :preferred_div => [ :share_issue, :share_repurch, :minority_int, :common_div, :net_income, :oci, :preferred_div ] } 19 | 20 | def summary(args) 21 | summary_cache_key = args[:period].to_pretty_s 22 | summary = lookup_cached_summary(summary_cache_key) 23 | return summary if !summary.nil? && false # FIXME: get rid of "and false" 24 | 25 | mapping = Xbrlware::ValueMapping.new 26 | mapping.policy[:unknown] = :no_action 27 | mapping.policy[:credit] = :no_action 28 | mapping.policy[:debit] = :flip 29 | 30 | summary = super(:period => args[:period], :mapping => mapping) 31 | if !lookup_cached_classifications(BASE_FILENAME, summary.rows) || true # FIXME: get rid of "or true" 32 | lookahead = [2, summary.rows.length-1].min 33 | classify_rows(ALL_STATES, NEXT_STATES, summary.rows, FinModeling::EquityChangeItem, lookahead) 34 | save_cached_classifications(BASE_FILENAME, summary.rows) 35 | end 36 | 37 | save_cached_summary(summary_cache_key, summary) 38 | 39 | return summary 40 | end 41 | 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/finmodeling/equity_change_item.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class EquityChangeItem < String 3 | include HasStringClassifier 4 | 5 | BASE_FILENAME = File.join(FinModeling::BASE_PATH, "classifiers/eci_") 6 | TYPES = [ :share_issue, :minority_int, :share_repurch, :common_div, # transactions with shareholders 7 | :net_income, :oci, :preferred_div ] # comprehensive income 8 | 9 | # Notes: 10 | # 1. I need to go back to the EquityChangeCalculation and make sure that it's value mapping policy is accurate 11 | 12 | # Questions: 13 | # 1. what about transactions involving options, warrants, convertible debt? 14 | # 2. what about transactions involving restricted stock? 15 | # 3. what do I do with the stock-based compensation items? 16 | # 4. I'm not tagging dividends here. Should I be? 17 | # 5. None of these items has preferred stock. ... Find an example, so the classifier knows. 18 | 19 | has_string_classifier(TYPES, EquityChangeItem) 20 | 21 | def self.load_vectors_and_train 22 | self._load_vectors_and_train(BASE_FILENAME, FinModeling::EquityChangeItem::TRAINING_VECTORS) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/finmodeling/factory.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class Factory 3 | def self.IncomeStatementCalculation(args = {}) 4 | entity_details = {} 5 | title = "" 6 | role = "" 7 | href = "" 8 | arcs = [] 9 | contexts = nil 10 | 11 | calculation = Xbrlware::Linkbase::CalculationLinkbase::Calculation.new(entity_details, title, role, href, arcs, contexts) 12 | return FinModeling::IncomeStatementCalculation.new(calculation) 13 | end 14 | 15 | def self.BalanceSheetCalculation(args = {}) 16 | entity_details = {} 17 | title = "" 18 | role = "" 19 | href = "" 20 | arcs = [] 21 | contexts = nil 22 | 23 | calculation = Xbrlware::Linkbase::CalculationLinkbase::Calculation.new(entity_details, title, role, href, arcs, contexts) 24 | return FinModeling::BalanceSheetCalculation.new(calculation) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/finmodeling/float_helpers.rb: -------------------------------------------------------------------------------- 1 | class Fixnum 2 | def to_nearest_million(num_decimals=1) 3 | return (self/1000000.0*(10.0**num_decimals)).round.to_f/(10.0**num_decimals) 4 | end 5 | def to_nearest_thousand(num_decimals=1) 6 | return (self/1000.0*(10.0**num_decimals)).round.to_f/(10.0**num_decimals) 7 | end 8 | def to_nearest_dollar(num_decimals=1) 9 | return ((self*(10.0**num_decimals)).round/(10.0**num_decimals)).to_f 10 | end 11 | end 12 | 13 | class Float 14 | def to_nearest_million(num_decimals=1) 15 | return (self/1000000.0*(10.0**num_decimals)).round.to_f/(10.0**num_decimals) 16 | end 17 | def to_nearest_thousand(num_decimals=1) 18 | return (self/1000.0*(10.0**num_decimals)).round.to_f/(10.0**num_decimals) 19 | end 20 | def to_nearest_dollar(num_decimals=1) 21 | return ((self*(10.0**num_decimals)).round/(10.0**num_decimals)).to_f 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/finmodeling/forecasted_reformulated_balance_sheet.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class ForecastedReformulatedBalanceSheet < ReformulatedBalanceSheet 3 | def initialize(period, noa, nfa, cse) 4 | @period = period 5 | @noa = noa 6 | @nfa = nfa 7 | @cse = cse 8 | 9 | @minority_interest = FinModeling::CalculationSummary.new 10 | end 11 | 12 | def operating_assets 13 | nil 14 | end 15 | 16 | def financial_assets 17 | nil 18 | end 19 | 20 | def operating_liabilities 21 | nil 22 | end 23 | 24 | def financial_liabilities 25 | nil 26 | end 27 | 28 | def net_operating_assets 29 | cs = FinModeling::CalculationSummary.new 30 | cs.title = "Net Operational Assets" 31 | cs.rows = [ CalculationRow.new( :key => "NOA", :vals => [@noa] ) ] 32 | return cs 33 | end 34 | 35 | def net_financial_assets 36 | cs = FinModeling::CalculationSummary.new 37 | cs.title = "Net Financial Assets" 38 | cs.rows = [ CalculationRow.new( :key => "NFA", :vals => [@nfa] ) ] 39 | return cs 40 | end 41 | 42 | def common_shareholders_equity 43 | cs = FinModeling::CalculationSummary.new 44 | cs.title = "Common Shareholders' Equity" 45 | cs.rows = [ CalculationRow.new( :key => "CSE", :vals => [@cse] ) ] 46 | return cs 47 | end 48 | 49 | def analysis(prev) 50 | analysis = super(prev) 51 | analysis.header_row.vals[0] += "E" # for estimated 52 | return analysis 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/finmodeling/forecasted_reformulated_income_statement.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class ForecastedReformulatedIncomeStatement < ReformulatedIncomeStatement 3 | def initialize(period, operating_revenues, income_from_sales_after_tax, net_financing_income, comprehensive_income) 4 | @period = period 5 | @orev = operating_revenues 6 | @income_from_sales_after_tax = income_from_sales_after_tax 7 | @net_financing_income = net_financing_income 8 | @comprehensive_income = comprehensive_income 9 | end 10 | 11 | def -(ris2) 12 | raise RuntimeError.new("not implmeneted") 13 | end 14 | 15 | def operating_revenues 16 | cs = FinModeling::CalculationSummary.new 17 | cs.title = "Operating Revenues" 18 | cs.rows = [ CalculationRow.new(:key => "Operating Revenues (OR)", :vals => [@orev] ) ] 19 | return cs 20 | end 21 | 22 | def cost_of_revenues 23 | nil 24 | end 25 | 26 | def gross_revenue 27 | nil 28 | end 29 | 30 | def operating_expenses 31 | nil 32 | end 33 | 34 | def income_from_sales_before_tax 35 | nil 36 | end 37 | 38 | def income_from_sales_after_tax 39 | cs = FinModeling::CalculationSummary.new 40 | cs.title = "Operating Income from sales, after tax (OISAT)" 41 | cs.rows = [ CalculationRow.new(:key => "Operating income from sales (after tax)", :vals => [@income_from_sales_after_tax] ) ] 42 | return cs 43 | end 44 | 45 | def operating_income_after_tax 46 | income_from_sales_after_tax # this simplified version assumes no non-sales operating income 47 | end 48 | 49 | def net_financing_income 50 | cs = FinModeling::CalculationSummary.new 51 | cs.title = "Net financing income, after tax (NFI)" 52 | cs.rows = [ CalculationRow.new(:key => "Net financing income", :vals => [@net_financing_income] ) ] 53 | return cs 54 | end 55 | 56 | def comprehensive_income 57 | cs = FinModeling::CalculationSummary.new 58 | cs.title = "Comprehensive Income (CI)" 59 | cs.rows = [ CalculationRow.new(:key => "Comprehensive income", :vals => [@comprehensive_income] ) ] 60 | return cs 61 | end 62 | 63 | def analysis(re_bs, prev_re_is, prev_re_bs, expected_cost_of_capital) 64 | analysis = CalculationSummary.new 65 | analysis.title = "" 66 | analysis.rows = [] 67 | 68 | if re_bs.nil? 69 | analysis.header_row = CalculationHeader.new(:key => "", :vals => ["Unknown..."]) 70 | else 71 | analysis.header_row = CalculationHeader.new(:key => "", :vals => [re_bs.period.to_pretty_s + "E"]) 72 | end 73 | 74 | analysis.rows << CalculationRow.new(:key => "Revenue ($MM)", :vals => [operating_revenues.total.to_nearest_million]) 75 | if Config.income_detail_enabled? 76 | analysis.rows << CalculationRow.new(:key => "COGS ($MM)", :vals => [nil]) 77 | analysis.rows << CalculationRow.new(:key => "GM ($MM)", :vals => [nil]) 78 | analysis.rows << CalculationRow.new(:key => "OE ($MM)", :vals => [nil]) 79 | analysis.rows << CalculationRow.new(:key => "OISBT ($MM)", :vals => [nil]) 80 | end 81 | analysis.rows << CalculationRow.new(:key => "Core OI ($MM)", :vals => [income_from_sales_after_tax.total.to_nearest_million]) 82 | analysis.rows << CalculationRow.new(:key => "OI ($MM)", :vals => [nil]) 83 | analysis.rows << CalculationRow.new(:key => "FI ($MM)", :vals => [net_financing_income.total.to_nearest_million]) 84 | analysis.rows << CalculationRow.new(:key => "NI ($MM)", :vals => [comprehensive_income.total.to_nearest_million]) 85 | analysis.rows << CalculationRow.new(:key => "Gross Margin", :vals => [nil]) 86 | analysis.rows << CalculationRow.new(:key => "Sales PM", :vals => [sales_profit_margin]) 87 | analysis.rows << CalculationRow.new(:key => "Operating PM", :vals => [nil]) 88 | analysis.rows << CalculationRow.new(:key => "FI / Sales", :vals => [fi_over_sales]) 89 | analysis.rows << CalculationRow.new(:key => "NI / Sales", :vals => [ni_over_sales]) 90 | 91 | if !prev_re_bs.nil? && !prev_re_is.nil? 92 | analysis.rows << CalculationRow.new(:key => "Sales / NOA", :vals => [sales_over_noa(prev_re_bs)]) 93 | analysis.rows << CalculationRow.new(:key => "FI / NFA", :vals => [fi_over_nfa( prev_re_bs)]) 94 | analysis.rows << CalculationRow.new(:key => "Revenue Growth",:vals => [revenue_growth(prev_re_is)]) 95 | analysis.rows << CalculationRow.new(:key => "Core OI Growth",:vals => [core_oi_growth(prev_re_is)]) 96 | analysis.rows << CalculationRow.new(:key => "OI Growth", :vals => [nil]) 97 | analysis.rows << CalculationRow.new(:key => "ReOI ($MM)", :vals => [re_oi(prev_re_bs, expected_cost_of_capital).to_nearest_million]) 98 | else 99 | analysis.rows << CalculationRow.new(:key => "Sales / NOA", :vals => [nil]) 100 | analysis.rows << CalculationRow.new(:key => "FI / NFA", :vals => [nil]) 101 | analysis.rows << CalculationRow.new(:key => "Revenue Growth",:vals => [nil]) 102 | analysis.rows << CalculationRow.new(:key => "Core OI Growth",:vals => [nil]) 103 | analysis.rows << CalculationRow.new(:key => "OI Growth", :vals => [nil]) 104 | analysis.rows << CalculationRow.new(:key => "ReOI ($MM)", :vals => [nil]) 105 | end 106 | 107 | return analysis 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/finmodeling/forecasts.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class Forecasts 3 | attr_accessor :reformulated_income_statements, :reformulated_balance_sheets 4 | 5 | def initialize 6 | @reformulated_income_statements = [] 7 | @reformulated_balance_sheets = [] 8 | end 9 | 10 | def balance_sheet_analyses(filings) 11 | if !@balance_sheet_analyses 12 | prev_filing = filings.last 13 | prev_re_bs = prev_filing.balance_sheet.reformulated(prev_filing.balance_sheet.periods.last) 14 | @reformulated_balance_sheets.each do |re_bs| 15 | next_analysis = re_bs.analysis(prev_re_bs) 16 | 17 | @balance_sheet_analyses = @balance_sheet_analyses + next_analysis if @balance_sheet_analyses 18 | @balance_sheet_analyses = next_analysis if !@balance_sheet_analyses 19 | prev_re_bs = re_bs 20 | end 21 | @balance_sheet_analyses = BalanceSheetAnalyses.new(@balance_sheet_analyses) 22 | end 23 | return @balance_sheet_analyses 24 | end 25 | 26 | def income_statement_analyses(filings, expected_rate_of_return) 27 | if !@income_statement_analyses 28 | prev_filing = filings.last 29 | prev_re_bs = prev_filing.balance_sheet.reformulated(prev_filing.balance_sheet.periods.last) 30 | prev_prev_is = (filings.length > 2) ? filings[-2].income_statement : nil 31 | prev_re_is = prev_filing.income_statement.latest_quarterly_reformulated(prev_cis=nil, prev_prev_is, prev_prev_cis=nil) 32 | 33 | @reformulated_income_statements.zip(@reformulated_balance_sheets).each do |re_is, re_bs| 34 | next_analysis = FinModeling::ReformulatedIncomeStatement.empty_analysis if !re_is 35 | next_analysis = re_is.analysis(re_bs, prev_re_is, prev_re_bs, expected_rate_of_return) if re_is 36 | 37 | @income_statement_analyses = @income_statement_analyses + next_analysis if @income_statement_analyses 38 | @income_statement_analyses = next_analysis if !@income_statement_analyses 39 | 40 | prev_re_bs, prev_re_is = [re_bs, re_is] 41 | end 42 | @income_statement_analyses = IncomeStatementAnalyses.new(@income_statement_analyses) 43 | end 44 | return @income_statement_analyses 45 | end 46 | 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/finmodeling/has_string_classifer.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | 3 | module HasStringClassifier 4 | module ClassMethods 5 | @klasses ||= [] 6 | @classifiers ||= {} 7 | @item_klass ||= nil 8 | 9 | def has_string_classifier(klasses, item_klass) 10 | @klasses = klasses 11 | @item_klass = item_klass 12 | @classifiers = Hash[ *klasses.zip(klasses.map{ |x| NaiveBayes.new(:yes, :no) }).flatten ] 13 | end 14 | 15 | def _load_vectors_and_train(base_filename, vectors) 16 | FileUtils.mkdir_p(File.dirname(base_filename)) if !File.exists?(File.dirname(base_filename)) 17 | success = FinModeling::Config.caching_enabled? 18 | @klasses.each do |cur_klass| 19 | filename = base_filename + cur_klass.to_s + ".db" 20 | success = success && File.exists?(filename) 21 | if success 22 | @classifiers[cur_klass] = NaiveBayes.load(filename) 23 | else 24 | @classifiers[cur_klass].db_filepath = filename 25 | end 26 | end 27 | return if success 28 | 29 | vectors.each do |vector| 30 | begin 31 | item = @item_klass.new(vector[:item_string]) 32 | item.train(vector[:klass]) 33 | rescue Exception => e 34 | puts "\"#{vector[:item_string]}\" has a bogus klass: \"#{vector[:klass]}\"" 35 | puts "\t" + e.message 36 | puts "\t" + e.backtrace.inspect.gsub(/, /, "\n\t ") 37 | end 38 | end 39 | 40 | @klasses.each do |cur_klass| 41 | @classifiers[cur_klass].save 42 | end 43 | end 44 | 45 | def klasses 46 | @klasses 47 | end 48 | 49 | def classifiers 50 | @classifiers 51 | end 52 | end 53 | 54 | def self.included(base) 55 | base.extend(ClassMethods) 56 | end 57 | 58 | def train(expected_klass) 59 | raise TypeError.new("#{expected_klass} is not in #{self.class.klasses}") if !self.class.klasses.include?(expected_klass) 60 | 61 | self.class.klasses.each do |cur_klass| 62 | is_expected_klass = (expected_klass == cur_klass) ? :yes : :no 63 | self.class.classifiers[cur_klass].train(is_expected_klass, *tokenize) 64 | end 65 | end 66 | 67 | def classification_estimates 68 | tokens = tokenize 69 | 70 | estimates = {} 71 | self.class.klasses.each do |cur_klass| 72 | ret = self.class.classifiers[cur_klass].classify(*tokens) 73 | result = {:klass=>ret[0], :confidence=>ret[1]} 74 | estimates[cur_klass] = (result[:klass] == :yes) ? result[:confidence] : -result[:confidence] 75 | end 76 | 77 | return estimates 78 | end 79 | 80 | def classify 81 | estimates = classification_estimates 82 | best_guess_klass = estimates.keys.sort{ |x,y| estimates[x] <=> estimates[y] }.last 83 | return best_guess_klass 84 | end 85 | 86 | def tokenize 87 | words = ["^"] + self.downcase.split(" ") + ["$"] 88 | 89 | tokens = [1, 2, 3].collect do |words_per_token| 90 | words.each_cons(words_per_token).to_a.map{|x| x.join(" ") } 91 | end 92 | return tokens.flatten 93 | end 94 | end 95 | 96 | end 97 | -------------------------------------------------------------------------------- /lib/finmodeling/income_statement_analyses.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module FinModeling 4 | 5 | class IncomeStatementAnalyses < CalculationSummary 6 | def initialize(calc_summary) 7 | @title = calc_summary.title 8 | @rows = calc_summary.rows 9 | @header_row = calc_summary.header_row 10 | @key_width = calc_summary.key_width 11 | @val_width = calc_summary.val_width 12 | @max_decimals = calc_summary.max_decimals 13 | @totals_row_enabled = false 14 | end 15 | 16 | def print_regressions 17 | if revenue_growth_row && revenue_growth_row.valid_vals.any? 18 | lr = revenue_growth_row.valid_vals.linear_regression 19 | puts "\t\trevenue growth: "+ 20 | "a:#{lr.a.to_s.cap_decimals(4)}, "+ 21 | "b:#{lr.b.to_s.cap_decimals(4)}, "+ 22 | "r²:#{lr.r2.to_s.cap_decimals(4)}, "+ 23 | "σ²:#{revenue_growth_row.valid_vals.variance.to_s.cap_decimals(4)}, " + 24 | ( (lr.r2 > 0.6) ? "strong fit" : ( (lr.r2 < 0.2) ? "weak fit [**]" : "avg fit") ) 25 | end 26 | 27 | if sales_over_noa_row && sales_over_noa_row.valid_vals.any? 28 | lr = sales_over_noa_row.valid_vals.linear_regression 29 | puts "\t\tsales / noa: "+ 30 | "a:#{lr.a.to_s.cap_decimals(4)}, "+ 31 | "b:#{lr.b.to_s.cap_decimals(4)}, "+ 32 | "r²:#{lr.r2.to_s.cap_decimals(4)}, "+ 33 | "σ²:#{sales_over_noa_row.valid_vals.variance.to_s.cap_decimals(4)}, " + 34 | ( (lr.r2 > 0.6) ? "strong fit" : ( (lr.r2 < 0.2) ? "weak fit [**]" : "avg fit") ) 35 | end 36 | 37 | if operating_pm_row && operating_pm_row.valid_vals.any? 38 | lr = operating_pm_row.valid_vals.linear_regression 39 | puts "\t\toperating pm: "+ 40 | "a:#{lr.a.to_s.cap_decimals(4)}, "+ 41 | "b:#{lr.b.to_s.cap_decimals(4)}, "+ 42 | "r²:#{lr.r2.to_s.cap_decimals(4)}, "+ 43 | "σ²:#{operating_pm_row.valid_vals.variance.to_s.cap_decimals(4)}, " + 44 | ( (lr.r2 > 0.6) ? "strong fit" : ( (lr.r2 < 0.2) ? "weak fit [**]" : "avg fit") ) 45 | end 46 | 47 | if fi_over_nfa_row && fi_over_nfa_row.valid_vals.any? 48 | lr = fi_over_nfa_row.valid_vals.linear_regression 49 | puts "\t\tfi / nfa: "+ 50 | "a:#{lr.a.to_s.cap_decimals(4)}, "+ 51 | "b:#{lr.b.to_s.cap_decimals(4)}, "+ 52 | "r²:#{lr.r2.to_s.cap_decimals(4)}, "+ 53 | "σ²:#{fi_over_nfa_row.valid_vals.variance.to_s.cap_decimals(4)}, " + 54 | ( (lr.r2 > 0.6) ? "strong fit" : ( (lr.r2 < 0.2) ? "weak fit [**]" : "avg fit") ) 55 | end 56 | end 57 | 58 | def revenue_growth_row 59 | find_row_by_key('Revenue Growth') 60 | end 61 | 62 | def operating_pm_row 63 | find_row_by_key('Operating PM') 64 | end 65 | 66 | def sales_over_noa_row 67 | find_row_by_key('Sales / NOA') 68 | end 69 | 70 | def fi_over_nfa_row 71 | find_row_by_key('FI / NFA') 72 | end 73 | 74 | def find_row_by_key(key) 75 | self.rows.find{ |x| x.key == key } 76 | end 77 | end 78 | 79 | end 80 | 81 | -------------------------------------------------------------------------------- /lib/finmodeling/income_statement_calculation.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class IncomeStatementCalculation < CompanyFilingCalculation 3 | include CanChooseSuccessivePeriods 4 | 5 | NI_GOAL = "net income" 6 | NI_LABELS = [ /^(|consolidated )net (|income loss|loss income|income|loss|)(| net of tax)(| attributable to parent)/, 7 | /^profit loss$/, # I have a feeling this is from the misguided attempt to parse CI here. Get rid of it... 8 | /^allocation.*of.*undistributed.*earnings/ ] 9 | NI_ANTI_LABELS = [ ] 10 | NI_IDS = [ /^(|Locator_|loc_)(|us-gaap_)NetIncomeLoss[_0-9a-z]+/, 11 | /^(|Locator_|loc_)(|us-gaap_)NetIncomeLossAvailableToCommonStockholdersBasic[_0-9a-z]+/, 12 | /^(|Locator_|loc_)(|us-gaap_)ProfitLoss[_0-9a-z]+/ ] 13 | def net_income_calculation 14 | begin 15 | @ni ||= NetIncomeCalculation.new(find_calculation_arc(NI_GOAL, NI_LABELS, NI_ANTI_LABELS, NI_IDS)) 16 | rescue FinModeling::InvalidFilingError => e 17 | pre_msg = "calculation tree:\n" + self.calculation.sprint_tree 18 | raise e, pre_msg+e.message, e.backtrace 19 | end 20 | end 21 | 22 | def is_valid? 23 | puts "income statement's net income calculation lacks tax item" if !net_income_calculation.has_tax_item? 24 | puts "income statement's net income calculation lacks sales/revenue item" if !net_income_calculation.has_revenue_item? 25 | if !net_income_calculation.has_tax_item? || !net_income_calculation.has_revenue_item? 26 | if net_income_calculation 27 | puts "summary:" 28 | net_income_calculation.summary(:period => periods.last).print 29 | end 30 | puts "calculation tree:\n" + self.calculation.sprint_tree(indent_count=0, simplified=true) 31 | end 32 | return (net_income_calculation.has_revenue_item? && net_income_calculation.has_tax_item?) 33 | end 34 | 35 | def reformulated(period, comprehensive_income_calculation) 36 | return ReformulatedIncomeStatement.new(period, 37 | net_income_calculation.summary(:period=>period), 38 | comprehensive_income_calculation ? comprehensive_income_calculation.summary(:period=>period) : nil) 39 | end 40 | 41 | def latest_quarterly_reformulated(cur_ci_calc, prev_is, prev_ci_calc) 42 | if net_income_calculation.periods.quarterly.any? 43 | period = net_income_calculation.periods.quarterly.last 44 | lqr = reformulated(period, cur_ci_calc) 45 | 46 | if (lqr.operating_revenues.total.abs > 1.0) && # FIXME: make an is_valid here? 47 | (lqr.cost_of_revenues .total.abs > 1.0) # FIXME: make an is_valid here? 48 | return lqr 49 | end 50 | end 51 | 52 | return nil if !prev_is 53 | 54 | cur_period, prev_period = choose_successive_periods(net_income_calculation, prev_is.net_income_calculation) 55 | if cur_period && prev_period 56 | new_re_is = reformulated(cur_period, cur_ci_calc) - prev_is.reformulated(prev_period, prev_ci_calc) 57 | # the above subtraction doesn't know what period you want. So let's patch the result to have 58 | # a quarterly period with the right end-points 59 | new_re_is.period = Xbrlware::Context::Period.new({"start_date"=>prev_period.value["end_date"], 60 | "end_date" =>cur_period.value["end_date"]}) 61 | return new_re_is 62 | end 63 | 64 | return nil 65 | end 66 | 67 | def write_constructor(file, item_name) 68 | item_calc_name = item_name + "_calc" 69 | @calculation.write_constructor(file, item_calc_name) 70 | file.puts "#{item_name} = FinModeling::IncomeStatementCalculation.new(#{item_calc_name})" 71 | end 72 | 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/finmodeling/income_statement_item.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class IncomeStatementItem < String 3 | include HasStringClassifier 4 | 5 | BASE_FILENAME = File.join(FinModeling::BASE_PATH, "classifiers/isi_") 6 | TYPES = [ :or, :cogs, :oe, :oibt, :fibt, :tax, :ooiat, :ooiat_nci, :fiat ] 7 | 8 | has_string_classifier(TYPES, IncomeStatementItem) 9 | 10 | def self.load_vectors_and_train 11 | self._load_vectors_and_train(BASE_FILENAME, FinModeling::IncomeStatementItem::TRAINING_VECTORS) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/finmodeling/invalid_filing_error.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class InvalidFilingError < StandardError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/finmodeling/liabs_and_equity_calculation.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class LiabsAndEquityCalculation < CompanyFilingCalculation 3 | include CanCacheClassifications 4 | include CanCacheSummaries 5 | include CanClassifyRows 6 | 7 | BASE_FILENAME = File.join(FinModeling::BASE_PATH, "summaries/liabs_and_equity_") 8 | 9 | ALL_STATES = [ :ol, :fl, :cse, :mi ] 10 | NEXT_STATES = { nil => [ :ol, :fl, :cse, :mi ], 11 | :ol => [ :ol, :fl, :cse, :mi ], # operating liabilities 12 | :fl => [ :ol, :fl, :cse, :mi ], # financial liabilities 13 | :cse => [ :ol, :fl, :cse, :mi ], # common shareholder equity 14 | :mi => [ :fl, :cse, :mi ] } # minority interest 15 | 16 | def summary(args) 17 | summary_cache_key = args[:period].to_pretty_s 18 | summary = lookup_cached_summary(summary_cache_key) 19 | return summary if !summary.nil? 20 | 21 | summary = super(:period => args[:period], :mapping => mapping) # FIXME: flip_total should == true! 22 | if !lookup_cached_classifications(BASE_FILENAME, summary.rows) 23 | lookahead = [4, summary.rows.length-1].min 24 | classify_rows(ALL_STATES, NEXT_STATES, summary.rows, FinModeling::LiabsAndEquityItem, lookahead) 25 | save_cached_classifications(BASE_FILENAME, summary.rows) 26 | end 27 | 28 | save_cached_summary(summary_cache_key, summary) 29 | 30 | return summary 31 | end 32 | 33 | def mapping 34 | m = Xbrlware::ValueMapping.new 35 | m.policy[:debit] = :flip 36 | m 37 | end 38 | 39 | def has_equity_item 40 | @has_equity_item ||= leaf_items.any? do |leaf| 41 | leaf.name.downcase.matches_any_regex?([/equity/, /stock/]) 42 | end 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/finmodeling/liabs_and_equity_item.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class LiabsAndEquityItem < String 3 | include HasStringClassifier 4 | 5 | BASE_FILENAME = File.join(FinModeling::BASE_PATH, "classifiers/laei_") 6 | TYPES = [ :ol, :fl, :cse, :mi ] 7 | 8 | has_string_classifier(TYPES, LiabsAndEquityItem) 9 | 10 | def self.load_vectors_and_train 11 | self._load_vectors_and_train(BASE_FILENAME, FinModeling::LiabsAndEquityItem::TRAINING_VECTORS) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/finmodeling/linear_trend_forecasting_policy.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class LinearTrendForecastingPolicy 3 | def initialize(args) 4 | @vals = args 5 | end 6 | 7 | def revenue_on(date) 8 | @vals[:revenue_estimator].estimate_on(date) 9 | end 10 | 11 | def sales_pm_on(date) 12 | @vals[:sales_pm_estimator].estimate_on(date) 13 | end 14 | 15 | def fi_over_nfa_on(date) 16 | @vals[:fi_over_nfa_estimator].estimate_on(date) 17 | end 18 | 19 | def sales_over_noa_on(date) 20 | @vals[:sales_over_noa_estimator].estimate_on(date) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/finmodeling/net_income_calculation.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class NetIncomeCalculation < CompanyFilingCalculation 3 | include CanCacheClassifications 4 | include CanCacheSummaries 5 | include CanClassifyRows 6 | 7 | BASE_FILENAME = File.join(FinModeling::BASE_PATH, "summaries/net_income_") 8 | 9 | ALL_STATES = [ :or, :cogs, :oe, :oibt, :fibt, :tax, :ooiat, :ooiat_nci, :fiat ] 10 | NEXT_STATES = { nil => [ :or ], 11 | :or => [ :or, :cogs, :oe, :oibt, :fibt ], 12 | :cogs => [ :cogs, :oe, :oibt, :fibt, :tax ], 13 | :oe => [ :oe, :oibt, :fibt, :tax ], 14 | :oibt => [ :oibt, :fibt, :tax ], # obit/fibt can cycle back/forth 15 | :fibt => [ :obit, :fibt, :tax ], # obit/fibt can cycle back/forth 16 | :tax => [ :ooiat, :ooiat_nci, :fiat ], # tax can't go to itself. only 1 such item. 17 | :ooiat => [ :ooiat, :ooiat_nci, :fiat ], # other operating income after taxes 18 | :ooiat_nci => [ :ooiat, :ooiat_nci, :fiat ], # ooiat related to non-controlling interest 19 | :fiat => [ :ooiat, :ooiat_nci, :fiat ] }# financing income after taxes 20 | 21 | def summary(args) 22 | summary_cache_key = args[:period].to_pretty_s 23 | thesummary = lookup_cached_summary(summary_cache_key) 24 | return thesummary if !thesummary.nil? 25 | 26 | mapping = Xbrlware::ValueMapping.new 27 | mapping.policy[:debit] = :flip 28 | 29 | thesummary = super(:period => args[:period], :mapping => mapping) # FIXME: flip_total should == true! 30 | if !lookup_cached_classifications(BASE_FILENAME, thesummary.rows) 31 | lookahead = [4, thesummary.rows.length-1].min 32 | classify_rows(ALL_STATES, NEXT_STATES, thesummary.rows, FinModeling::IncomeStatementItem, lookahead) 33 | save_cached_classifications(BASE_FILENAME, thesummary.rows) 34 | end 35 | 36 | save_cached_summary(summary_cache_key, thesummary) 37 | 38 | return thesummary 39 | end 40 | 41 | def has_revenue_item? 42 | @has_revenue_item ||= leaf_items.any? do |leaf| 43 | leaf.name.matches_any_regex?([/revenue/i, /sales/i]) 44 | end 45 | end 46 | 47 | def has_tax_item? 48 | @has_tax_item ||= leaf_items.any? do |leaf| 49 | leaf.name.matches_any_regex?([/tax/i]) 50 | end 51 | end 52 | 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/finmodeling/net_income_summary_from_differences.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | 3 | class NetIncomeSummaryFromDifferences 4 | def initialize(ris1, ris2) 5 | @ris1 = ris1 6 | @ris2 = ris2 7 | end 8 | 9 | def filter_by_type(key) 10 | cs = FinModeling::CalculationSummary.new 11 | case key 12 | when :or 13 | cs.title = "Operating Revenues" 14 | cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.operating_revenues.total] ), 15 | CalculationRow.new(:key => "Second Row", :vals => [-@ris2.operating_revenues.total] ) ] 16 | when :cogs 17 | cs.title = "Cost of Revenues" 18 | cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.cost_of_revenues.total] ), 19 | CalculationRow.new(:key => "Second Row", :vals => [-@ris2.cost_of_revenues.total] ) ] 20 | when :oe 21 | cs.title = "Operating Expenses" 22 | cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.operating_expenses.total] ), 23 | CalculationRow.new(:key => "Second Row", :vals => [-@ris2.operating_expenses.total] ) ] 24 | when :oibt 25 | cs.title = "Operating Income from Sales, Before taxes" 26 | cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.operating_income_after_tax.rows[1].vals.first] ), 27 | CalculationRow.new(:key => "Second Row", :vals => [-@ris2.operating_income_after_tax.rows[1].vals.first] ) ] 28 | when :fibt 29 | cs.title = "Financing Income, Before Taxes" 30 | cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.net_financing_income.rows[0].vals.first] ), 31 | CalculationRow.new(:key => "Second Row", :vals => [-@ris2.net_financing_income.rows[0].vals.first] ) ] 32 | when :tax 33 | cs.title = "Taxes" 34 | cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.income_from_sales_after_tax.rows[1].vals.first] ), 35 | CalculationRow.new(:key => "Second Row", :vals => [-@ris2.income_from_sales_after_tax.rows[1].vals.first] ) ] 36 | when :ooiat 37 | cs.title = "Other Operating Income, After Taxes" 38 | cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.operating_income_after_tax.rows[3].vals.first] ), 39 | CalculationRow.new(:key => "Second Row", :vals => [-@ris2.operating_income_after_tax.rows[3].vals.first] ) ] 40 | when :fiat 41 | cs.title = "Financing Income, After Taxes" 42 | cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.net_financing_income.rows[2].vals.first] ), 43 | CalculationRow.new(:key => "Second Row", :vals => [-@ris2.net_financing_income.rows[2].vals.first] ) ] 44 | else 45 | return nil 46 | end 47 | return cs 48 | end 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /lib/finmodeling/paths.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | 3 | BASE_PATH = File.expand_path("~/.finmodeling") 4 | 5 | end 6 | -------------------------------------------------------------------------------- /lib/finmodeling/period_array.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class PeriodArray < Array 3 | def quarterly 4 | PeriodArray.new(self.select{ |x| x.is_duration? && 5 | (Xbrlware::DateUtil.days_between(x.value["end_date"], x.value["start_date"]) >= 2*28) && 6 | (Xbrlware::DateUtil.days_between(x.value["end_date"], x.value["start_date"]) <= 4*31) }) 7 | end 8 | 9 | def halfyearly 10 | PeriodArray.new(self.select{ |x| x.is_duration? && 11 | (Xbrlware::DateUtil.days_between(x.value["end_date"], x.value["start_date"]) >= 5*30) && 12 | (Xbrlware::DateUtil.days_between(x.value["end_date"], x.value["start_date"]) <= 7*31) }) 13 | end 14 | 15 | def threequarterly 16 | PeriodArray.new(self.select{ |x| x.is_duration? && 17 | (Xbrlware::DateUtil.days_between(x.value["end_date"], x.value["start_date"]) >= 8*30) && 18 | (Xbrlware::DateUtil.days_between(x.value["end_date"], x.value["start_date"]) <= 10*31) }) 19 | end 20 | 21 | def yearly 22 | PeriodArray.new(self.select{ |x| x.is_duration? && 23 | (Xbrlware::DateUtil.days_between(x.value["end_date"], x.value["start_date"]) >= 11*30) && 24 | (Xbrlware::DateUtil.days_between(x.value["end_date"], x.value["start_date"]) <= 13*31) }) 25 | end 26 | end 27 | end 28 | 29 | -------------------------------------------------------------------------------- /lib/finmodeling/quarterly_report_filing.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class QuarterlyReportFiling < AnnualReportFiling 3 | 4 | def write_constructor(file, item_name) 5 | balance_sheet .write_constructor(file, bs_name = item_name + "_bs" ) 6 | income_statement .write_constructor(file, is_name = item_name + "_is" ) if has_an_income_statement? 7 | comprehensive_income_statement.write_constructor(file, cis_name = item_name + "_cis") if has_a_comprehensive_income_statement? 8 | cash_flow_statement .write_constructor(file, cfs_name = item_name + "_cfs") 9 | 10 | is_name = "nil" if !has_an_income_statement? 11 | cis_name = "nil" if !has_a_comprehensive_income_statement? 12 | ses_name = "nil" # because these only get reported in 10-k's? 13 | 14 | names_of_discs = [] 15 | disclosures.each_with_index do |disclosure, idx| 16 | name_of_disc = item_name + "_disc#{idx}" 17 | disclosure.write_constructor(file, name_of_disc) 18 | names_of_discs << name_of_disc 19 | end 20 | names_of_discs_str = "[" + names_of_discs.join(',') + "]" 21 | 22 | file.puts "#{SCHEMA_VERSION_ITEM} = #{CURRENT_SCHEMA_VERSION}" 23 | 24 | file.puts "#{item_name} = FinModeling::CachedQuarterlyFiling.new(#{bs_name}, #{is_name}, #{cis_name}, #{cfs_name}, #{ses_name}, #{names_of_discs_str})" 25 | end 26 | 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/finmodeling/rate.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class Rate 3 | attr_reader :value 4 | 5 | def initialize(value) 6 | @value = value 7 | end 8 | 9 | def annualize(from_days=365, to_days=365) 10 | ((1.0 + @value)**(to_days.to_f/from_days.to_f)) - 1.0 11 | end 12 | 13 | def yearly_to_quarterly 14 | annualize(from_days=365.0, to_days=365.0/4.0) 15 | end 16 | 17 | def quarterly_to_yearly 18 | annualize(from_days=365.0/4.0, to_days=365.0) 19 | end 20 | end 21 | 22 | class DiscountRate < Rate 23 | def annualize(from_days=365, to_days=365) 24 | @value**(to_days.to_f/from_days.to_f) 25 | end 26 | end 27 | end 28 | 29 | -------------------------------------------------------------------------------- /lib/finmodeling/ratio.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class Ratio 3 | def initialize(value) 4 | @value = value 5 | end 6 | 7 | def annualize(from_days=365, to_days=365) 8 | @value*(to_days.to_f/from_days.to_f) 9 | end 10 | 11 | def yearly_to_quarterly 12 | annualize(from_days=365.0, to_days=365.0/4.0) 13 | end 14 | 15 | def quarterly_to_yearly 16 | annualize(from_days=365.0/4.0, to_days=365.0) 17 | end 18 | end 19 | end 20 | 21 | -------------------------------------------------------------------------------- /lib/finmodeling/reformulated_cash_flow_statement.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class ReformulatedCashFlowStatement 3 | attr_accessor :period 4 | 5 | def initialize(period, cash_change_summary) 6 | @period = period 7 | 8 | @c = cash_change_summary.filter_by_type(:c) # just make this a member.... 9 | @i = cash_change_summary.filter_by_type(:i) 10 | @d = cash_change_summary.filter_by_type(:d) 11 | @f = cash_change_summary.filter_by_type(:f) 12 | 13 | @c.title = "Cash from operations" 14 | @i.title = "Cash investments in operations" 15 | @d.title = "Payments to debtholders" 16 | @f.title = "Payments to stockholders" 17 | 18 | if !(cash_change_summary.is_a? CashChangeSummaryFromDifferences) 19 | @d.rows << CalculationRow.new(:key => "Investment in Cash and Equivalents", 20 | :type => :d, 21 | :vals => [-cash_change_summary.total]) 22 | end 23 | end 24 | 25 | def -(re_cfs2) 26 | summary = CashChangeSummaryFromDifferences.new(self, re_cfs2) 27 | return ReformulatedCashFlowStatement.new(@period, summary) 28 | end 29 | 30 | def cash_from_operations 31 | @c 32 | end 33 | 34 | def cash_investments_in_operations 35 | @i 36 | end 37 | 38 | def payments_to_debtholders 39 | @d 40 | end 41 | 42 | def payments_to_stockholders 43 | @f 44 | end 45 | 46 | def free_cash_flow 47 | cs = FinModeling::CalculationSummary.new 48 | cs.title = "Free Cash Flow" 49 | cs.rows = [ CalculationRow.new(:key => "Cash from Operations (C)", :vals => [@c.total] ), 50 | CalculationRow.new(:key => "Cash Investment in Operations (I)", :vals => [@i.total] ) ] 51 | return cs 52 | end 53 | 54 | def financing_flows 55 | cs = FinModeling::CalculationSummary.new 56 | cs.title = "Financing Flows" 57 | cs.rows = [ CalculationRow.new(:key => "Payments to debtholders (d)", :vals => [@d.total] ), 58 | CalculationRow.new(:key => "Payments to stockholders (F)", :vals => [@f.total] ) ] 59 | return cs 60 | end 61 | 62 | def ni_over_c(inc_stmt) 63 | inc_stmt.comprehensive_income.total.to_f / cash_from_operations.total 64 | end 65 | 66 | def self.empty_analysis 67 | analysis = CalculationSummary.new 68 | 69 | analysis.title = "" 70 | analysis.header_row = CalculationHeader.new(:key => "", :vals => ["Unknown..."]) 71 | 72 | analysis.rows = [] 73 | analysis.rows << CalculationRow.new(:key => "C ($MM)", :vals => [nil]) 74 | analysis.rows << CalculationRow.new(:key => "I ($MM)", :vals => [nil]) 75 | analysis.rows << CalculationRow.new(:key => "d ($MM)", :vals => [nil]) 76 | analysis.rows << CalculationRow.new(:key => "F ($MM)", :vals => [nil]) 77 | analysis.rows << CalculationRow.new(:key => "FCF ($MM)", :vals => [nil]) 78 | analysis.rows << CalculationRow.new(:key => "NI / C", :vals => [nil]) 79 | 80 | return analysis 81 | end 82 | 83 | def analysis(inc_stmt) 84 | analysis = CalculationSummary.new 85 | 86 | analysis.title = "" 87 | analysis.header_row = CalculationHeader.new(:key => "", :vals => [@period.value["end_date"].to_s]) 88 | 89 | analysis.rows = [] 90 | analysis.rows << CalculationRow.new(:key => "C ($MM)", :vals => [cash_from_operations.total.to_nearest_million]) 91 | analysis.rows << CalculationRow.new(:key => "I ($MM)", :vals => [cash_investments_in_operations.total.to_nearest_million]) 92 | analysis.rows << CalculationRow.new(:key => "d ($MM)", :vals => [payments_to_debtholders.total.to_nearest_million]) 93 | analysis.rows << CalculationRow.new(:key => "F ($MM)", :vals => [payments_to_stockholders.total.to_nearest_million]) 94 | analysis.rows << CalculationRow.new(:key => "FCF ($MM)", :vals => [free_cash_flow.total.to_nearest_million]) 95 | if inc_stmt 96 | analysis.rows << CalculationRow.new(:key => "NI / C", :vals => [ni_over_c(inc_stmt)]) 97 | else 98 | analysis.rows << CalculationRow.new(:key => "NI / C", :vals => [nil]) 99 | end 100 | 101 | return analysis 102 | end 103 | 104 | ALLOWED_IMBALANCE = 1.0 105 | def flows_are_balanced? 106 | (free_cash_flow.total - financing_flows.total) < ALLOWED_IMBALANCE 107 | end 108 | 109 | def flows_are_plausible? 110 | return [ payments_to_debtholders, 111 | payments_to_stockholders, 112 | cash_from_operations, 113 | cash_investments_in_operations ].all?{ |x| x.total.abs > 1.0 } 114 | end 115 | 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/finmodeling/reformulated_shareholder_equity_statement.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class ReformulatedShareholderEquityStatement 3 | attr_accessor :period 4 | 5 | def initialize(period, equity_change_summary) 6 | @period = period 7 | 8 | @share_issue = equity_change_summary.filter_by_type(:share_issue ) 9 | @minority_int = equity_change_summary.filter_by_type(:minority_int ) 10 | @share_repurch = equity_change_summary.filter_by_type(:share_repurch) 11 | @common_div = equity_change_summary.filter_by_type(:common_div ) 12 | @net_income = equity_change_summary.filter_by_type(:net_income ) 13 | @oci = equity_change_summary.filter_by_type(:oci ) 14 | @preferred_div = equity_change_summary.filter_by_type(:preferred_div) 15 | end 16 | 17 | def transactions_with_shareholders 18 | cs = FinModeling::CalculationSummary.new 19 | cs.title = "Transactions with Shareholders" 20 | cs.rows = [ CalculationRow.new(:key => "Share Issues", :vals => [@share_issue .total] ), 21 | CalculationRow.new(:key => "Minority Interest", :vals => [@minority_int .total] ), 22 | CalculationRow.new(:key => "Share Repurchases", :vals => [@share_repurch.total] ), 23 | CalculationRow.new(:key => "Common Dividends", :vals => [@common_div .total] ) ] 24 | return cs 25 | end 26 | 27 | def comprehensive_income 28 | cs = FinModeling::CalculationSummary.new 29 | cs.title = "Comprehensive Income" 30 | cs.rows = [ CalculationRow.new(:key => "Net Income", :vals => [@net_income .total] ), 31 | CalculationRow.new(:key => "Other Comprehensive Income", :vals => [@oci .total] ), 32 | CalculationRow.new(:key => "Preferred Dividends", :vals => [@preferred_div.total] ) ] 33 | return cs 34 | end 35 | 36 | def analysis 37 | analysis = CalculationSummary.new 38 | 39 | analysis.title = "" 40 | analysis.header_row = CalculationHeader.new(:key => "", :vals => [@period.value["end_date"].to_s]) 41 | 42 | analysis.rows = [] 43 | analysis.rows << CalculationRow.new(:key => "Tx w Shareholders ($MM)", :vals => [transactions_with_shareholders.total.to_nearest_million]) 44 | analysis.rows << CalculationRow.new(:key => "CI ($MM)", :vals => [comprehensive_income.total.to_nearest_million]) 45 | 46 | return analysis 47 | end 48 | 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/finmodeling/reoi_valuation.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class ReOIValuation 3 | def initialize(filings, forecasts, cost_of_capital, num_shares) 4 | @filings, @forecasts, @cost_of_capital, @num_shares = [filings, forecasts, cost_of_capital, num_shares] 5 | @discount_rate = FinModeling::DiscountRate.new(@cost_of_capital.value + 1.0) 6 | end 7 | 8 | def summary 9 | s = CalculationSummary.new 10 | s.title = "ReOI Valuation" 11 | s.totals_row_enabled = false 12 | 13 | s.header_row = CalculationHeader.new(:key => "", :vals => periods.map{ |x| x.to_pretty_s + ((x.value > Date.today) ? "E" : "") }) 14 | 15 | s.rows = [ ] 16 | 17 | s.rows << CalculationRow.new(:key => "ReOI ($MM)", :vals => reoi_vals.map{ |x| x ? x.to_nearest_million : nil }) 18 | s.rows << CalculationRow.new(:key => "PV(ReOI) ($MM)", :vals => pv_reoi_vals.map{ |x| x ? x.to_nearest_million : nil }) 19 | s.rows << CalculationRow.new(:key => "CV ($MM)", :vals => cv_vals.map{ |x| x ? x.to_nearest_million : nil }) 20 | s.rows << CalculationRow.new(:key => "PV(CV) ($MM)", :vals => pv_cv_vals.map{ |x| x ? x.to_nearest_million : nil }) 21 | s.rows << CalculationRow.new(:key => "Book Value of Common Equity ($MM)", :vals => bv_cse_vals.map{ |x| x ? x.to_nearest_million : nil }) 22 | s.rows << CalculationRow.new(:key => "Enterprise Value ($MM)", :vals => ev_vals.map{ |x| x ? x.to_nearest_million : nil }) 23 | s.rows << CalculationRow.new(:key => "NFA ($MM)", :vals => bv_nfa_vals.map{ |x| x ? x.to_nearest_million : nil }) 24 | s.rows << CalculationRow.new(:key => "Value of Common Equity ($MM)", :vals => cse_vals.map{ |x| x ? x.to_nearest_million : nil }) 25 | s.rows << CalculationRow.new(:key => "# Shares (MM)", :vals => num_shares_vals.map{ |x| x ? x.to_nearest_million : nil }) 26 | s.rows << CalculationRow.new(:key => "Value / Share ($)", :vals => val_per_shares_vals.map{ |x| x ? x.to_nearest_dollar(num_decimals=2) : nil }) 27 | 28 | return s 29 | end 30 | 31 | def periods 32 | @periods ||= [ @filings.re_bs_arr.last.period ] + 33 | @forecasts.reformulated_balance_sheets.map{ |x| x.period } 34 | end 35 | 36 | private 37 | 38 | def reoi_vals 39 | prev_re_bses = [@filings.re_bs_arr.last] + @forecasts.reformulated_balance_sheets[0..-2] 40 | re_ises = @forecasts.reformulated_income_statements 41 | re_ois = [nil] + re_ises.zip(prev_re_bses).map{ |pair| pair[0].re_oi(pair[1], @cost_of_capital.value) } 42 | end 43 | 44 | def pv_reoi_vals 45 | reoi_vals[0..-2].each_with_index.map do |reoi, idx| 46 | days_from_now = periods[idx].value - Date.today 47 | d = @discount_rate.annualize(from_days=365, to_days=days_from_now) 48 | reoi ? (reoi / d) : nil 49 | end + [nil] 50 | end 51 | 52 | def cv_vals 53 | vals = [nil]*periods.length 54 | d = @cost_of_capital.annualize(from_days=365, @forecasts.reformulated_income_statements.last.period.days) 55 | vals[-2] = reoi_vals.last / d 56 | vals 57 | end 58 | 59 | def pv_cv_vals 60 | cv_vals[0..-2].each_with_index.map do |cv, idx| 61 | days_from_now = periods[idx].value - Date.today 62 | d = @discount_rate.annualize(from_days=365, to_days=days_from_now) 63 | cv ? (cv / d) : nil 64 | end + [nil] 65 | end 66 | 67 | def bv_cse_vals 68 | vals = [nil]*periods.length 69 | vals[0] = @filings.re_bs_arr.last.common_shareholders_equity.total 70 | vals 71 | end 72 | 73 | def ev_vals 74 | vals = [nil]*periods.length 75 | vals[0] = (pv_reoi_vals[1..-2] + pv_cv_vals[-2..-2] + bv_cse_vals[0..0]).inject(:+) 76 | vals 77 | end 78 | 79 | def bv_nfa_vals 80 | vals = [nil]*periods.length 81 | vals[0] = @filings.re_bs_arr.last.net_financial_assets.total 82 | vals 83 | end 84 | 85 | def cse_vals 86 | vals = [nil]*periods.length 87 | vals[0] = ev_vals[0] + bv_nfa_vals[0] 88 | vals 89 | end 90 | 91 | def num_shares_vals 92 | vals = [nil]*periods.length 93 | vals[0] = @num_shares 94 | vals 95 | end 96 | 97 | def val_per_shares_vals 98 | vals = [nil]*periods.length 99 | vals[0] = cse_vals[0] / @num_shares 100 | vals 101 | end 102 | 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/finmodeling/shareholder_equity_statement_calculation.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class ShareholderEquityStatementCalculation < CompanyFilingCalculation 3 | #include CanChooseSuccessivePeriods 4 | 5 | EC_GOAL = "change in shareholder equity" 6 | EC_LABELS = [ /^stockholders equity period increase decrease$/ ] 7 | EC_ANTI_LABELS = [ ] 8 | EC_IDS = [ /^(|Locator_|loc_)(|us-gaap_)StockholdersEquityPeriodIncreaseDecrease[_a-z0-9]+/ ] 9 | def equity_change_calculation 10 | begin 11 | @ec ||= EquityChangeCalculation.new(find_calculation_arc(EC_GOAL, EC_LABELS, EC_ANTI_LABELS, EC_IDS)) 12 | rescue FinModeling::InvalidFilingError => e 13 | pre_msg = "calculation tree:\n" + self.calculation.sprint_tree 14 | raise e, pre_msg+e.message, e.backtrace 15 | end 16 | end 17 | 18 | def is_valid? 19 | return true 20 | end 21 | 22 | def reformulated(period) 23 | return ReformulatedShareholderEquityStatement.new(period, 24 | equity_change_calculation.summary(:period=>period)) 25 | end 26 | 27 | def write_constructor(file, item_name) 28 | item_calc_name = item_name + "_calc" 29 | @calculation.write_constructor(file, item_calc_name) 30 | file.puts "#{item_name} = FinModeling::ShareholderEquityStatementCalculation.new(#{item_calc_name})" 31 | end 32 | 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/finmodeling/string_helpers.rb: -------------------------------------------------------------------------------- 1 | class String 2 | def fixed_width_left_justify(width) 3 | return self[0..(width-1 )] if self.length == width 4 | return self[0..(width-1-3)]+"..." if self.length > width 5 | return self + (" " * (width - self.length)) 6 | end 7 | 8 | def fixed_width_right_justify(width) 9 | return self[(-width )..-1] if self.length == width 10 | return "..."+self[(-width+3)..-1] if self.length > width 11 | return (" " * (width - self.length)) + self 12 | end 13 | 14 | def with_thousands_separators 15 | self.reverse.scan(/(?:\d*\.)?\d{1,3}-?/).join(',').reverse 16 | end 17 | 18 | def cap_decimals(num_decimals) 19 | r = Regexp.new('(.*\.[0-9]{' + num_decimals.to_s + '})[0-9]*') 20 | self.gsub(r, '\1') 21 | end 22 | 23 | def matches_any_regex?(regexes) 24 | return regexes.inject(false){ |matches, regex| matches or regex =~ self } 25 | end 26 | 27 | def split_into_lines_shorter_than(max_line_width) 28 | lines = [] 29 | cur_line = [] 30 | 31 | split(' ').each do |word| 32 | if (cur_line + [word]).join(' ').length > max_line_width 33 | lines << cur_line.join(' ') 34 | cur_line = [] 35 | end 36 | 37 | cur_line << word 38 | end 39 | 40 | lines << cur_line.join(' ') if !cur_line.empty? 41 | lines 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/finmodeling/time_series_estimator.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class TimeSeriesEstimator 3 | attr_reader :a, :b 4 | 5 | def initialize(a, b) 6 | @a, @b = a, b 7 | end 8 | 9 | def estimate_on(date) 10 | x = (date - Date.today) 11 | a + (b*x) 12 | end 13 | 14 | def self.from_time_series(dates, ys) 15 | xs = dates.map{ |date| date - Date.today } 16 | 17 | simple_regression = Statsample::Regression.simple(xs.to_scale, ys.to_scale) 18 | TimeSeriesEstimator.new(simple_regression.a, simple_regression.b) 19 | end 20 | 21 | def self.from_const(y) 22 | TimeSeriesEstimator.new(a=y.first, b=0.0) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/finmodeling/trailing_avg_forecasting_policy.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class TrailingAvgForecastingPolicy 3 | def initialize(args) 4 | @vals = args 5 | end 6 | 7 | def revenue_on(date) 8 | @vals[:revenue_estimator].estimate_on(date) 9 | end 10 | 11 | def sales_pm_on(date) 12 | @vals[:sales_pm_estimator].estimate_on(date) 13 | end 14 | 15 | def fi_over_nfa_on(date) 16 | @vals[:fi_over_nfa_estimator].estimate_on(date) 17 | end 18 | 19 | def sales_over_noa_on(date) 20 | @vals[:sales_over_noa_estimator].estimate_on(date) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/finmodeling/version.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | VERSION = "0.2.1" 3 | end 4 | -------------------------------------------------------------------------------- /lib/finmodeling/weighted_avg_cost_of_capital.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | class WeightedAvgCostOfCapital 3 | attr_reader :rate 4 | 5 | def initialize(equity_market_val, debt_market_val, cost_of_equity, after_tax_cost_of_debt) 6 | @equity_market_val = equity_market_val 7 | @debt_market_val = debt_market_val 8 | @cost_of_equity = cost_of_equity 9 | @after_tax_cost_of_debt = after_tax_cost_of_debt 10 | 11 | e_weight = @equity_market_val / (@equity_market_val + @debt_market_val) 12 | d_weight = @debt_market_val / (@equity_market_val + @debt_market_val) 13 | 14 | @rate = Rate.new((e_weight * @cost_of_equity.value) + (d_weight * @after_tax_cost_of_debt.value)) 15 | end 16 | 17 | def summary 18 | s = CalculationSummary.new 19 | s.title = "Cost of Capital" 20 | s.totals_row_enabled = false 21 | 22 | s.header_row = CalculationHeader.new(:key => "", :vals => [Date.today.to_s]) 23 | 24 | s.rows = [ ] 25 | 26 | s.rows << CalculationRow.new(:key => "Market Value of Equity ($MM)", :vals => [@equity_market_val.to_nearest_million]) 27 | s.rows << CalculationRow.new(:key => "Market Value of Debt ($MM)", :vals => [@debt_market_val.to_nearest_million]) 28 | s.rows << CalculationRow.new(:key => "Cost of Equity (%)", :vals => [sprintf("%.2f", 100.0*@cost_of_equity.value)]) 29 | s.rows << CalculationRow.new(:key => "Cost of Debt (%)", :vals => [sprintf("%.2f", 100.0*@after_tax_cost_of_debt.value)]) 30 | s.rows << CalculationRow.new(:key => "Weighted Avg Cost of Capital (%)", :vals => [sprintf("%.2f", 100.0*@rate.value)]) 31 | 32 | return s 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/finmodeling/yahoo_finance_helpers.rb: -------------------------------------------------------------------------------- 1 | module YahooFinance 2 | def YahooFinance.get_market_cap(stock_symbol) 3 | quote = YahooFinance::get_quotes(YahooFinance::ExtendedQuote, stock_symbol).values.first 4 | m = /([0-9\.]*)([MB])/.match(quote.marketCap) 5 | mkt_cap = m[1].to_f 6 | case 7 | when m[2]=="M" 8 | mkt_cap *= 1000*1000 9 | when m[2]=="B" 10 | mkt_cap *= 1000*1000*1000 11 | end 12 | mkt_cap 13 | end 14 | 15 | def YahooFinance.get_num_shares(stock_symbol) 16 | mkt_cap = YahooFinance.get_market_cap(stock_symbol) 17 | share_price = YahooFinance::get_quotes(YahooFinance::StandardQuote, stock_symbol).values.last.lastTrade.to_f 18 | mkt_cap / share_price 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/annual_report_filing_spec.rb: -------------------------------------------------------------------------------- 1 | # annual_report_filing_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::AnnualReportFiling do 6 | before(:all) do 7 | company = FinModeling::Company.new(FinModeling::Mocks::Entity.new) 8 | filing_url = company.annual_reports.last.link 9 | @filing = FinModeling::AnnualReportFiling.download(filing_url) 10 | end 11 | 12 | subject { @filing } 13 | its(:balance_sheet) { should be_a FinModeling::BalanceSheetCalculation } 14 | its(:income_statement) { should be_a FinModeling::IncomeStatementCalculation } 15 | its(:cash_flow_statement) { should be_a FinModeling::CashFlowStatementCalculation } 16 | 17 | context "when the report doesn't have a comprehensive income statement" do 18 | its(:has_a_comprehensive_income_statement?) { should be_false } 19 | describe ".comprehensive_income_statement" do 20 | it "should raise an InvalidFilingError" do 21 | expect{subject.comprehensive_income_statement}.to raise_error(FinModeling::InvalidFilingError) 22 | end 23 | end 24 | end 25 | context "when the report doesn't have a statement of shareholders' equity" do 26 | its(:has_a_shareholder_equity_statement?) { should be_false } 27 | describe ".shareholder_equity_statement" do 28 | it "should raise an InvalidFilingError" do 29 | expect{subject.shareholder_equity_statement}.to raise_error(FinModeling::InvalidFilingError) 30 | end 31 | end 32 | its(:is_valid?) { should == [@filing.income_statement, 33 | @filing.balance_sheet, 34 | @filing.cash_flow_statement].all?{|x| x.is_valid?} } 35 | end 36 | context "when the report has a statement of shareholders' equity" do 37 | before(:all) do 38 | filing_url = "http://www.sec.gov/Archives/edgar/data/315189/000110465910063219/0001104659-10-063219-index.htm" 39 | FinModeling::Config::disable_caching 40 | @filing = FinModeling::AnnualReportFiling.download filing_url 41 | FinModeling::Config::enable_caching 42 | end 43 | subject { @filing } 44 | 45 | its(:has_a_shareholder_equity_statement?) { should be_true } 46 | its(:shareholder_equity_statement) { should be_a FinModeling::ShareholderEquityStatementCalculation } 47 | its(:is_valid?) { should == [@filing.income_statement, 48 | @filing.balance_sheet, 49 | @filing.cash_flow_statement, 50 | @filing.shareholder_equity_statement].all?{|x| x.is_valid?} } 51 | 52 | context "after write_constructor()ing it to a file and then eval()ing the results" do 53 | before(:all) do 54 | file_name = "/tmp/finmodeling-annual-rpt.rb" 55 | schema_version_item_name = "@schema_version" 56 | item_name = "@annual_rpt" 57 | file = File.open(file_name, "w") 58 | @filing.write_constructor(file, item_name) 59 | file.close 60 | 61 | eval(File.read(file_name)) 62 | 63 | @schema_version = eval(schema_version_item_name) 64 | @loaded_filing = eval(item_name) 65 | end 66 | 67 | specify { @schema_version.should be == 1.3 } 68 | 69 | subject { @loaded_filing } 70 | its(:balance_sheet) { should have_the_same_periods_as(@filing.balance_sheet) } 71 | its(:balance_sheet) { should have_the_same_reformulated_last_total(:net_operating_assets).as(@filing.balance_sheet) } 72 | its(:income_statement) { should have_the_same_reformulated_last_total(:net_financing_income).as(@filing.income_statement) } 73 | its(:cash_flow_statement) { should have_the_same_last_total(:cash_change_calculation).as(@filing.cash_flow_statement) } 74 | its(:shareholder_equity_statement) { should be_a FinModeling::ShareholderEquityStatementCalculation } 75 | its(:shareholder_equity_statement) { should have_the_same_last_total(:equity_change_calculation).as(@filing.shareholder_equity_statement) } 76 | its(:disclosures) { should have_the_same_last_total(:first).as(@filing.disclosures) } 77 | end 78 | end 79 | 80 | context "when the report has a comprehensive income statement" do 81 | before(:all) do 82 | filing_url = "http://www.sec.gov/Archives/edgar/data/818479/000081847912000023/0000818479-12-000023-index.htm" 83 | FinModeling::Config::disable_caching 84 | @filing = FinModeling::AnnualReportFiling.download filing_url 85 | FinModeling::Config::enable_caching 86 | end 87 | subject { @filing } 88 | 89 | its(:has_a_comprehensive_income_statement?) { should be_true } 90 | its(:comprehensive_income_statement) { should be_a FinModeling::ComprehensiveIncomeStatementCalculation } 91 | 92 | context "after write_constructor()ing it to a file and then eval()ing the results" do 93 | before(:all) do 94 | file_name = "/tmp/finmodeling-annual-rpt.rb" 95 | schema_version_item_name = "@schema_version" 96 | item_name = "@annual_rpt" 97 | file = File.open(file_name, "w") 98 | @filing.write_constructor(file, item_name) 99 | file.close 100 | 101 | eval(File.read(file_name)) 102 | @loaded_filing = eval(item_name) 103 | end 104 | 105 | subject { @loaded_filing } 106 | its(:comprehensive_income_statement) { should be_a FinModeling::ComprehensiveIncomeStatementCalculation } 107 | its(:comprehensive_income_statement) { should have_the_same_last_total(:comprehensive_income_calculation).as(@filing.comprehensive_income_statement) } 108 | end 109 | 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /spec/assets_calculation_spec.rb: -------------------------------------------------------------------------------- 1 | # assets_calculation_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::AssetsCalculation do 6 | before(:all) do 7 | google_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/1288776/000119312512025336/0001193125-12-025336-index.htm" 8 | filing = FinModeling::AnnualReportFiling.download google_2011_annual_rpt 9 | @bal_sheet = filing.balance_sheet 10 | 11 | @period = @bal_sheet.periods.last 12 | @a = @bal_sheet.assets_calculation 13 | end 14 | 15 | describe ".summary" do 16 | subject { @a.summary(:period=>@period) } 17 | it { should be_a FinModeling::CalculationSummary } 18 | end 19 | 20 | describe ".has_cash_item" do 21 | pending "Find a test case..." 22 | end 23 | end 24 | 25 | -------------------------------------------------------------------------------- /spec/assets_item_spec.rb: -------------------------------------------------------------------------------- 1 | # period_array_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::AssetsItem do 6 | 7 | before(:all) do 8 | #FinModeling::AssetsItem.load_vectors_and_train(FinModeling::AssetsItem::TRAINING_VECTORS) 9 | end 10 | 11 | describe "new" do 12 | subject { FinModeling::AssetsItem.new("Property Plant And Equipment Net") } 13 | it { should be_a FinModeling::AssetsItem } 14 | end 15 | 16 | describe "train" do 17 | subject { FinModeling::AssetsItem.new("Property Plant And Equipment Net") } 18 | it "trains the classifier that this AssetsItem is of the given type" do 19 | subject.train(:oa) 20 | end 21 | end 22 | 23 | describe "classification_estimates" do 24 | subject { FinModeling::AssetsItem.new("Property Plant And Equipment Net").classification_estimates } 25 | its(:keys) { should == FinModeling::AssetsItem::TYPES } 26 | end 27 | 28 | describe "classify" do 29 | let(:ai) { FinModeling::AssetsItem.new("Property Plant And Equipment Net") } 30 | subject { ai.classify } 31 | it "returns the AssetsItem type with the highest probability estimate" do 32 | estimates = ai.classification_estimates 33 | estimates[subject].should be_within(0.1).of(estimates.values.max) 34 | end 35 | end 36 | 37 | describe "load_vectors_and_train" do 38 | # the before(:all) clause calls load_vectors_and_train already 39 | # we can just focus, here, on its effects 40 | 41 | it "classifies >95% correctly" do 42 | num_items = 0 43 | errors = [] 44 | FinModeling::AssetsItem::TRAINING_VECTORS.each do |vector| 45 | num_items += 1 46 | ai = FinModeling::AssetsItem.new(vector[:item_string]) 47 | if ai.classify != vector[:klass] 48 | errors.push({ :ai=>ai.to_s, :expected=>vector[:klass], :got=>ai.classify }) 49 | end 50 | end 51 | 52 | pct_errors = errors.length.to_f / num_items 53 | if pct_errors > 0.05 54 | puts "errors: " + errors.inspect 55 | end 56 | pct_errors.should be < 0.05 57 | 58 | end 59 | end 60 | 61 | end 62 | -------------------------------------------------------------------------------- /spec/balance_sheet_analyses_spec.rb: -------------------------------------------------------------------------------- 1 | # balance_sheets_analyses_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::BalanceSheetAnalyses do 6 | before(:all) do 7 | @summary = FinModeling::CalculationSummary.new 8 | @summary.title = "Title 123" 9 | @summary.rows = [ ] 10 | @summary.rows << FinModeling::CalculationRow.new(:key => "NOA Growth", :type => :oa, :vals => [ 4]) 11 | @summary.rows << FinModeling::CalculationRow.new(:key => "Row", :type => :fa, :vals => [109]) 12 | @summary.rows << FinModeling::CalculationRow.new(:key => "Row", :type => :oa, :vals => [ 93]) 13 | @summary.rows << FinModeling::CalculationRow.new(:key => "Row", :type => :fa, :vals => [ 1]) 14 | end 15 | 16 | describe ".new" do 17 | subject { FinModeling::BalanceSheetAnalyses.new(@summary) } 18 | 19 | it { should be_a_kind_of FinModeling::CalculationSummary } 20 | its(:title) { should == @summary.title } 21 | its(:rows) { should == @summary.rows } 22 | its(:header_row) { should == @summary.header_row } 23 | its(:rows) { should == @summary.rows } 24 | its(:num_value_columns) { should == @summary.num_value_columns } 25 | its(:key_width) { should == @summary.key_width } 26 | its(:val_width) { should == @summary.val_width } 27 | its(:max_decimals) { should == @summary.max_decimals } 28 | its(:totals_row_enabled) { should be_false } 29 | end 30 | 31 | describe ".print_regressions" do 32 | subject { FinModeling::BalanceSheetAnalyses.new(@summary) } 33 | 34 | it { should respond_to(:print_regressions) } 35 | end 36 | 37 | describe ".noa_growth_row" do 38 | subject { FinModeling::BalanceSheetAnalyses.new(@summary).noa_growth_row } 39 | 40 | it { should be_a FinModeling::CalculationRow } 41 | its(:key) { should == "NOA Growth" } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/balance_sheet_calculation_spec.rb: -------------------------------------------------------------------------------- 1 | # balance_sheet_calculation_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::BalanceSheetCalculation do 6 | before(:all) do 7 | google_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/1288776/000119312512025336/0001193125-12-025336-index.htm" 8 | filing = FinModeling::AnnualReportFiling.download google_2011_annual_rpt 9 | @balance_sheet = filing.balance_sheet 10 | @period = @balance_sheet.periods.last 11 | end 12 | 13 | describe ".assets_calculation" do 14 | subject { @balance_sheet.assets_calculation } 15 | it { should be_a FinModeling::AssetsCalculation } 16 | its(:label) { should match /asset/i } 17 | 18 | let(:right_side_sum) { @balance_sheet.liabs_and_equity_calculation.leaf_items_sum(:period=>@period) } 19 | specify { subject.leaf_items_sum(:period=>@period).should be_within(1.0).of(right_side_sum) } 20 | end 21 | 22 | describe ".liabs_and_equity_calculation" do 23 | subject { @balance_sheet.liabs_and_equity_calculation} 24 | it { should be_a FinModeling::LiabsAndEquityCalculation } 25 | its(:label) { should match /liab.*equity/i } 26 | end 27 | 28 | describe ".is_valid?" do 29 | context "if none of the asset leaf nodes contains the term 'cash'" do 30 | it "returns false" do 31 | #ea_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/712515/000119312511149262/0001193125-11-149262-index.htm" 32 | #filing = FinModeling::AnnualReportFiling.download ea_2011_annual_rpt 33 | #filing.balance_sheet.is_valid?.should be_false 34 | pending "Need to find another example of this...." 35 | end 36 | end 37 | context "if none of the liability/equity net income leaf nodes contains the term 'equity'" do 38 | it "returns false" do 39 | #ea_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/712515/000119312511149262/0001193125-11-149262-index.htm" 40 | #filing = FinModeling::AnnualReportFiling.download ea_2011_annual_rpt 41 | #filing.balance_sheet.is_valid?.should be_false 42 | pending "Need to find another example of this...." 43 | end 44 | end 45 | context "if the assets total does not match the liabilities and equity total" do 46 | it "returns false" do 47 | #ea_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/712515/000119312511149262/0001193125-11-149262-index.htm" 48 | #filing = FinModeling::AnnualReportFiling.download ea_2011_annual_rpt 49 | #filing.balance_sheet.is_valid?.should be_false 50 | pending "Need to find another example of this...." 51 | end 52 | end 53 | context "otherwise" do 54 | it "returns true" do 55 | @balance_sheet.is_valid?.should be_true 56 | end 57 | end 58 | end 59 | 60 | describe ".reformulated" do 61 | subject { @balance_sheet.reformulated(@period) } 62 | it { should be_a FinModeling::ReformulatedBalanceSheet } 63 | end 64 | 65 | describe ".write_constructor" do 66 | before(:all) do 67 | file_name = "/tmp/finmodeling-bal-sheet.rb" 68 | item_name = "@bal_sheet" 69 | file = File.open(file_name, "w") 70 | @balance_sheet.write_constructor(file, item_name) 71 | file.close 72 | 73 | eval(File.read(file_name)) 74 | @loaded_bs = eval(item_name) 75 | end 76 | 77 | context "after write_constructor()ing it to a file and then eval()ing the results" do 78 | subject { @loaded_bs } 79 | it { should have_the_same_periods_as @balance_sheet } 80 | it { should have_the_same_reformulated_last_total(:net_operating_assets).as(@balance_sheet) } 81 | end 82 | end 83 | 84 | end 85 | 86 | -------------------------------------------------------------------------------- /spec/can_classify_rows_spec.rb: -------------------------------------------------------------------------------- 1 | # assets_calculation_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::CanClassifyRows do 6 | before(:all) do 7 | class AgeItem < String 8 | def classification_estimates 9 | case 10 | when self =~ /^1/ then {:teens=>1.0, :twenties=>0.0, :thirties=>0.0, :fourties=>0.0} 11 | when self =~ /^2/ then {:teens=>0.0, :twenties=>1.0, :thirties=>0.0, :fourties=>0.0} 12 | when self =~ /^3/ then {:teens=>0.0, :twenties=>0.0, :thirties=>1.0, :fourties=>0.0} 13 | when self =~ /^4/ then {:teens=>0.0, :twenties=>0.0, :thirties=>0.0, :fourties=>1.0} 14 | else {:teens=>0.0, :twenties=>0.0, :thirties=>0.0, :fourties=>0.0} 15 | end 16 | end 17 | end 18 | 19 | class AgeList 20 | attr_accessor :calculation 21 | 22 | include FinModeling::CanClassifyRows 23 | 24 | ALL_STATES = [ :teens, :twenties, :thirties, :fourties ] 25 | NEXT_STATES = { nil => [ :teens, :twenties, :thirties, :fourties ], 26 | :teens => [ :teens, :twenties, :thirties, :fourties ], 27 | :twenties => [ :twenties, :thirties, :fourties ], 28 | :thirties => [ :thirties, :fourties ], 29 | :fourties => [ :fourties ] } 30 | 31 | def classify(args) 32 | lookahead = [args[:max_lookahead], calculation.rows.length-1].min 33 | classify_rows(ALL_STATES, NEXT_STATES, calculation.rows, AgeItem, lookahead) 34 | end 35 | end 36 | end 37 | 38 | describe "classify_rows" do 39 | context "with 1 consecutive error" do 40 | before(:all) do 41 | @age_list = AgeList.new 42 | @age_list.calculation = FinModeling::CalculationSummary.new 43 | ages = [21, 41, 30, 35] 44 | @age_list.calculation.rows = ages.collect { |age| FinModeling::CalculationRow.new(:key => age.to_s, :vals => 0) } 45 | end 46 | context "with lookahead of 0" do 47 | it "should fail to correct errors" do 48 | expected_rows = [:twenties, :fourties, :fourties, :fourties] 49 | @age_list.classify(:max_lookahead=>0) 50 | @age_list.calculation.rows.map{ |row| row.type }.should == expected_rows 51 | end 52 | end 53 | context "with lookahead of 1" do 54 | it "should correct one error" do 55 | expected_rows = [:twenties, :twenties, :thirties, :thirties] 56 | @age_list.classify(:max_lookahead=>1) 57 | @age_list.calculation.rows.map{ |row| row.type }.should == expected_rows 58 | end 59 | end 60 | end 61 | context "with 2 consecutive errors" do 62 | before(:all) do 63 | @age_list = AgeList.new 64 | @age_list.calculation = FinModeling::CalculationSummary.new 65 | ages = [21, 41, 40, 25, 30, 35, 38, 40] 66 | @age_list.calculation.rows = ages.collect { |age| FinModeling::CalculationRow.new(:key => age.to_s, :vals => 0) } 67 | end 68 | context "with lookahead of 2" do 69 | it "should fail to correct errors" do 70 | expected_rows = [:twenties, :fourties, :fourties, :fourties, :fourties, :fourties, :fourties, :fourties] 71 | @age_list.classify(:max_lookahead=>2) 72 | @age_list.calculation.rows.map{ |row| row.type }.should == expected_rows 73 | end 74 | end 75 | context "with lookahead of 3" do 76 | it "should correct one error" do 77 | expected_rows = [:twenties, :twenties, :twenties, :twenties, :thirties, :thirties, :thirties, :fourties] 78 | @age_list.classify(:max_lookahead=>3) 79 | @age_list.calculation.rows.map{ |row| row.type }.should == expected_rows 80 | end 81 | end 82 | end 83 | 84 | end 85 | end 86 | 87 | -------------------------------------------------------------------------------- /spec/cash_change_calculation_spec.rb: -------------------------------------------------------------------------------- 1 | # cash_change_calculation_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::CashChangeCalculation do 6 | before(:all) do 7 | goog_2011_q3_report = "http://www.sec.gov/Archives/edgar/data/1288776/000119312511282235/0001193125-11-282235-index.htm" 8 | FinModeling::Config::disable_caching 9 | @filing = FinModeling::AnnualReportFiling.download(goog_2011_q3_report) 10 | FinModeling::Config::enable_caching 11 | @cfs_period_q1_thru_q3 = @filing.cash_flow_statement.periods.threequarterly.last 12 | 13 | @cash_changes = @filing.cash_flow_statement.cash_change_calculation 14 | 15 | bs_period_initial = @filing.balance_sheet.periods[-2] 16 | bs_period_final = @filing.balance_sheet.periods[-1] 17 | 18 | @cash_initial = @filing.balance_sheet.assets_calculation.summary(:period => bs_period_initial).rows[0].vals.first 19 | @cash_final = @filing.balance_sheet.assets_calculation.summary(:period => bs_period_final ).rows[0].vals.first 20 | end 21 | 22 | describe ".summary" do 23 | subject{ @cash_changes.summary(:period => @cfs_period_q1_thru_q3) } 24 | it { should be_an_instance_of FinModeling::CalculationSummary } 25 | 26 | it "should have values with the right sign" do 27 | expected = [7033, 1011, 337, 1437, -61, 526, 3, -247, 268, 28 | -146, 72, 255, 70, 83, -2487, -43693, 33107, 29 | -358, 694, -395, -1350, -20, 61, 0, 8780, -8054, 74] 30 | 31 | actual = subject.rows.map{|row| (row.vals.first/1000.0/1000.0).round} 32 | 33 | if actual != expected 34 | num_errors = actual.zip(expected).map{ |x,y| x==y ? 0 : 1 }.inject(:+) 35 | puts "# errors: #{num_errors}" 36 | end 37 | 38 | actual.should == expected 39 | end 40 | 41 | describe ".total" do 42 | subject{ @cash_changes.summary(:period => @cfs_period_q1_thru_q3).total } 43 | it { should be_within(1.0).of(@cash_final - @cash_initial) } 44 | end 45 | end 46 | end 47 | 48 | -------------------------------------------------------------------------------- /spec/cash_change_item_spec.rb: -------------------------------------------------------------------------------- 1 | # period_array_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::CashChangeItem do 6 | 7 | describe "new" do 8 | subject { FinModeling::CashChangeItem.new("Depreciation and amortization of property and equipment") } 9 | it { should be_a FinModeling::CashChangeItem } 10 | end 11 | 12 | describe "train" do 13 | let(:item) { FinModeling::CashChangeItem.new("Depreciation and amortization of property and equipment") } 14 | it "trains the classifier that this CashChangeItem is of the given type" do 15 | item.train(:c) 16 | end 17 | end 18 | 19 | describe "classification_estimates" do 20 | let(:item) { FinModeling::CashChangeItem.new("Depreciation and amortization of property and equipment") } 21 | subject { item.classification_estimates } 22 | its(:keys) { should == FinModeling::CashChangeItem::TYPES } 23 | end 24 | 25 | describe "classify" do 26 | let(:cci) { FinModeling::CashChangeItem.new("Depreciation and amortization of property and equipment") } 27 | subject { cci.classify } 28 | it "returns the CashChangeItem type with the highest probability estimate" do 29 | estimates = cci.classification_estimates 30 | estimates[subject].should be_within(0.1).of(estimates.values.max) 31 | end 32 | end 33 | 34 | describe "load_vectors_and_train" do 35 | # the before(:all) clause calls load_vectors_and_train already 36 | # we can just focus, here, on its effects 37 | 38 | it "classifies >92% correctly" do # FIXME: add more vectors to tighten this up 39 | num_items = 0 40 | errors = [] 41 | FinModeling::CashChangeItem::TRAINING_VECTORS.each do |vector| 42 | num_items += 1 43 | cci = FinModeling::CashChangeItem.new(vector[:item_string]) 44 | if cci.classify != vector[:klass] 45 | errors.push({ :cci=>cci.to_s, :expected=>vector[:klass], :got=>cci.classify }) 46 | end 47 | end 48 | 49 | pct_errors = errors.length.to_f / num_items 50 | if pct_errors > 0.08 51 | puts "errors: " + errors.inspect 52 | end 53 | pct_errors.should be < 0.08 54 | 55 | end 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /spec/cash_flow_statement_calculation_spec.rb: -------------------------------------------------------------------------------- 1 | # balance_sheet_calculation_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::CashFlowStatementCalculation do 6 | before(:all) do 7 | google_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/1288776/000119312512025336/0001193125-12-025336-index.htm" 8 | filing = FinModeling::AnnualReportFiling.download(google_2011_annual_rpt) 9 | @cash_flow_stmt = filing.cash_flow_statement 10 | @period = @cash_flow_stmt.periods.last 11 | end 12 | 13 | describe "cash_change_calculation" do 14 | subject { @cash_flow_stmt.cash_change_calculation } 15 | it { should be_an_instance_of FinModeling::CashChangeCalculation } 16 | its(:label) { should match /^cash/i } 17 | end 18 | 19 | describe "is_valid?" do 20 | it "returns true if free cash flow matches financing flows and none are zero" do 21 | re_cfs = @cash_flow_stmt.reformulated(@period) 22 | flows_are_balanced = (re_cfs.free_cash_flow.total == (-1*re_cfs.financing_flows.total)) 23 | none_are_zero = (re_cfs.cash_from_operations.total != 0) && 24 | (re_cfs.cash_investments_in_operations.total != 0) && 25 | (re_cfs.payments_to_debtholders.total != 0) && 26 | (re_cfs.payments_to_stockholders.total != 0) 27 | 28 | @cash_flow_stmt.is_valid?.should == (flows_are_balanced && none_are_zero) 29 | end 30 | end 31 | 32 | describe "reformulated" do 33 | subject { @cash_flow_stmt.reformulated(@period) } 34 | it { should be_a FinModeling::ReformulatedCashFlowStatement } 35 | end 36 | 37 | describe "latest_quarterly_reformulated" do 38 | before(:all) do 39 | google_2011_q1_rpt = "http://www.sec.gov/Archives/edgar/data/1288776/000119312511134428/0001193125-11-134428-index.htm" 40 | @cash_flow_stmt_2011_q1 = FinModeling::AnnualReportFiling.download(google_2011_q1_rpt).cash_flow_statement 41 | 42 | google_2011_q2_rpt = "http://www.sec.gov/Archives/edgar/data/1288776/000119312511199078/0001193125-11-199078-index.htm" 43 | @cash_flow_stmt_2011_q2 = FinModeling::AnnualReportFiling.download(google_2011_q2_rpt).cash_flow_statement 44 | 45 | google_2011_q3_rpt = "http://www.sec.gov/Archives/edgar/data/1288776/000119312511282235/0001193125-11-282235-index.htm" 46 | @cash_flow_stmt_2011_q3 = FinModeling::AnnualReportFiling.download(google_2011_q3_rpt).cash_flow_statement 47 | end 48 | 49 | context "when given a Q1 report" do 50 | subject { @cash_flow_stmt_2011_q1.latest_quarterly_reformulated(nil) } 51 | it { should be_an_instance_of FinModeling::ReformulatedCashFlowStatement } 52 | its(:cash_investments_in_operations) { should have_a_plausible_total } 53 | end 54 | 55 | context "when given a Q2 report (and a previous Q1 report)" do 56 | subject { @cash_flow_stmt_2011_q2.latest_quarterly_reformulated(@cash_flow_stmt_2011_q1) } 57 | it { should be_an_instance_of FinModeling::ReformulatedCashFlowStatement } 58 | it "should be valid" do 59 | subject.cash_investments_in_operations.total.abs.should be > 1.0 60 | end 61 | end 62 | 63 | context "when given a Q3 report (and a previous Q2 report)" do 64 | subject { @cash_flow_stmt_2011_q3.latest_quarterly_reformulated(@cash_flow_stmt_2011_q2) } 65 | it { should be_an_instance_of FinModeling::ReformulatedCashFlowStatement } 66 | its(:cash_investments_in_operations) { should have_a_plausible_total } 67 | end 68 | 69 | context "when given an annual report (and a previous Q3 report)" do 70 | subject { @cash_flow_stmt.latest_quarterly_reformulated(@cash_flow_stmt_2011_q3) } 71 | it { should be_an_instance_of FinModeling::ReformulatedCashFlowStatement } 72 | its(:cash_investments_in_operations) { should have_a_plausible_total } 73 | end 74 | end 75 | 76 | context "after write_constructor()ing it to a file and then eval()ing the results" do 77 | before(:all) do 78 | file_name = "/tmp/finmodeling-cash_flow_stmt.rb" 79 | item_name = "@cfs" 80 | file = File.open(file_name, "w") 81 | @cash_flow_stmt.write_constructor(file, item_name) 82 | file.close 83 | 84 | eval(File.read(file_name)) 85 | @loaded_cfs = eval(item_name) 86 | end 87 | 88 | subject { @loaded_cfs } 89 | it { should have_the_same_periods_as(@cash_flow_stmt) } 90 | its(:cash_change_calculation) { should have_the_same_last_total_as(@cash_flow_stmt.cash_change_calculation) } 91 | end 92 | 93 | end 94 | 95 | -------------------------------------------------------------------------------- /spec/company_beta_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'date' # FIXME: stuff this somewhere else? 3 | 4 | describe FinModeling::CAPM::Beta do 5 | describe "#from_ticker" do 6 | context "when num_days >= 31" do 7 | it "returns the right value" do 8 | index_ticker = "SPY" 9 | mock_index_quotes = [] 10 | company_ticker = "AAPL" 11 | mock_company_quotes = [] 12 | 13 | num_days = 90 # FIXME: must be greater than 30? 14 | 0.upto(num_days-1) do |day| 15 | date_str = (DateTime.new(2013, 2, 5) + day).strftime("%Y-%m-%d") 16 | mock_index_quotes << YahooFinance::HistoricalQuote.new(index_ticker, [date_str,153.6,154.7,153.6,154.2,121431900, 54.29 + day + rand]) 17 | mock_company_quotes << YahooFinance::HistoricalQuote.new(company_ticker, [date_str,153.6,154.7,153.6,154.2,121431900,154.29 + day + rand]) 18 | end 19 | 20 | YahooFinance.should_receive(:get_HistoricalQuotes_days).with(index_ticker, num_days).and_return(mock_index_quotes) 21 | YahooFinance.should_receive(:get_HistoricalQuotes_days).with(company_ticker, num_days).and_return(mock_company_quotes) 22 | 23 | common_dates = mock_index_quotes.map{ |x| x.date } & mock_company_quotes.map{ |x| x.date } 24 | 25 | index_daily_quotes = mock_index_quotes .select{ |x| common_dates.include?(x.date) }.sort{ |x,y| x.date <=> y.date } 26 | company_daily_quotes = mock_company_quotes.select{ |x| common_dates.include?(x.date) }.sort{ |x,y| x.date <=> y.date } 27 | 28 | index_monthly_quotes = index_daily_quotes .group_by{ |x| x.date.gsub(/-[0-9][0-9]$/, "") } 29 | .values 30 | .map{ |x| x.sort{ |x,y| x.date <=> y.date }.first } 31 | .sort{ |x,y| x.date <=> y.date }[1..-1] 32 | company_monthly_quotes = company_daily_quotes.group_by{ |x| x.date.gsub(/-[0-9][0-9]$/, "") } 33 | .values 34 | .map{ |x| x.sort{ |x,y| x.date <=> y.date }.first } 35 | .sort{ |x,y| x.date <=> y.date }[1..-1] 36 | 37 | index_monthly_returns = index_monthly_quotes.each_cons(2).map { |pair| (pair[1].adjClose-pair[0].adjClose)/pair[0].adjClose } 38 | company_monthly_returns = company_monthly_quotes.each_cons(2).map{ |pair| (pair[1].adjClose-pair[0].adjClose)/pair[0].adjClose } 39 | 40 | x = GSL::Vector.alloc(index_monthly_returns) 41 | y = GSL::Vector.alloc(company_monthly_returns) 42 | intercept, slope = GSL::Fit::linear(x, y) 43 | expected_beta = slope 44 | 45 | FinModeling::CAPM::Beta.from_ticker(company_ticker, num_days).should be_within(0.1).of(expected_beta) # FIXME: this is now ignorant of dividends... 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/company_filing_calculation_spec.rb: -------------------------------------------------------------------------------- 1 | # company_filing_calculation_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::CompanyFilingCalculation do 6 | before(:all) do 7 | google_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/1288776/000119312512025336/0001193125-12-025336-index.htm" 8 | @filing = FinModeling::AnnualReportFiling.download google_2011_annual_rpt 9 | end 10 | 11 | let(:calculation) { FinModeling::Mocks::Calculation.new } 12 | subject { FinModeling::CompanyFilingCalculation.new(calculation) } 13 | 14 | it { should be_a FinModeling::CompanyFilingCalculation } 15 | its(:label) { should == calculation.label } 16 | 17 | describe "periods" do 18 | subject { @filing.balance_sheet.periods } 19 | it { should be_a FinModeling::PeriodArray } 20 | specify { subject.map{ |x| x.to_s }.sort.should == ["2008-12-31", "2009-12-31", "2010-12-31", "2011-12-31"] } 21 | end 22 | 23 | describe "leaf_items" do 24 | let(:assets) { @filing.balance_sheet.assets_calculation } 25 | context "when given a period" do 26 | subject { assets.leaf_items(:period => @filing.balance_sheet.periods.last) } 27 | it "should contain Xbrlware::Item's" do 28 | subject.all?{ |x| x.class == Xbrlware::Item }.should be_true 29 | end 30 | it "returns the leaf items that match the period" do 31 | subject.should have(12).items 32 | end 33 | end 34 | context "when not given a period" do 35 | subject { assets.leaf_items } 36 | it "should contain Xbrlware::Item's" do 37 | subject.all?{ |x| x.class == Xbrlware::Item }.should be_true 38 | end 39 | it "returns all leaf items" do 40 | subject.should have(26).items 41 | end 42 | end 43 | end 44 | 45 | describe "leaf_items_sum" do 46 | context "given a balance sheet with items that subtract from the total (like accum. depreciation)" do 47 | it "returns the sum of the calculation tree, in the given period" do 48 | pending "can't parse this 10-k. need to find another suitable example" 49 | 50 | vepc_2010_annual_rpt = "http://www.sec.gov/Archives/edgar/data/103682/000119312511049905/d10k.htm" 51 | @filing_with_mixed_order = FinModeling::AnnualReportFiling.download vepc_2010_annual_rpt 52 | 53 | balance_sheet = @filing_with_mixed_order.balance_sheet 54 | @assets = balance_sheet.assets_calculation 55 | @period = balance_sheet.periods.last 56 | 57 | mapping = Xbrlware::ValueMapping.new 58 | mapping.policy[:credit] = :flip 59 | 60 | @assets.leaf_items_sum(:period=>@period, :mapping=>mapping).should be_within(1.0).of(42817000000.0) 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/company_filing_spec.rb: -------------------------------------------------------------------------------- 1 | # company_filing_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::CompanyFiling do 6 | let(:company) { FinModeling::Company.new(FinModeling::Mocks::Entity.new) } 7 | let(:filing_url) { company.annual_reports.last.link } 8 | 9 | subject { FinModeling::CompanyFiling.download filing_url } 10 | 11 | describe "#download" do 12 | it { should be_a FinModeling::CompanyFiling } 13 | end 14 | 15 | describe ".print_presentations" do 16 | it { should respond_to(:print_presentations) } 17 | end 18 | 19 | describe ".print_calculations" do 20 | it { should respond_to(:print_calculations) } 21 | end 22 | 23 | describe ".disclosures" do 24 | subject { (FinModeling::CompanyFiling.download filing_url).disclosures } 25 | 26 | it { should be_an_instance_of Array } 27 | it { should_not be_empty } 28 | its(:first) { should be_a FinModeling::CompanyFilingCalculation } 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/company_filings_spec.rb: -------------------------------------------------------------------------------- 1 | # company_filings_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::CompanyFilings do 6 | before (:all) do 7 | @company = FinModeling::Company.find("aapl") 8 | @filings = FinModeling::CompanyFilings.new(@company.filings_since_date(Time.parse("2010-10-01"))) 9 | end 10 | 11 | describe ".balance_sheet_analyses" do 12 | subject { @filings.balance_sheet_analyses } 13 | it { should be_a FinModeling::BalanceSheetAnalyses } 14 | it "should have one column per filing" do 15 | subject.num_value_columns.should == @filings.length 16 | end 17 | end 18 | 19 | describe ".cash_flow_statement_analyses" do 20 | subject { @filings.cash_flow_statement_analyses } 21 | it { should be_a FinModeling::CalculationSummary } 22 | it "should have one column per filing" do 23 | subject.num_value_columns.should == @filings.length 24 | end 25 | end 26 | 27 | describe ".income_statement_analyses" do 28 | subject { @filings.income_statement_analyses(e_ror=0.10) } 29 | it { should be_a FinModeling::IncomeStatementAnalyses } 30 | it "should have one column per filing" do 31 | subject.num_value_columns.should == @filings.length 32 | end 33 | end 34 | 35 | describe ".re_bs_arr" do 36 | subject { @filings.re_bs_arr } 37 | it "should be an array of FinModeling::ReformulatedBalanceSheet" do 38 | subject.all?{ |re_bs| re_bs.should be_a FinModeling::ReformulatedBalanceSheet } 39 | end 40 | it "should have one per filing" do 41 | subject.length.should == @filings.length 42 | end 43 | end 44 | 45 | describe ".re_is_arr" do 46 | subject { @filings.re_is_arr } 47 | it "should be an array whose first element is nil" do 48 | subject.first.should be_nil 49 | end 50 | it "should be an array whose remaining elements are FinModeling::ReformulatedIncomeStatement's" do 51 | subject[1..-1].all?{ |re_is| re_is.should be_a FinModeling::ReformulatedIncomeStatement } 52 | end 53 | it "should have one per filing" do 54 | subject.length.should == @filings.length 55 | end 56 | end 57 | 58 | describe ".disclosures" do 59 | context "when a yearly disclosure is requested" do 60 | subject { @filings.disclosures(/Disclosure Provision For Income Taxes/, :yearly) } 61 | it { should be_a FinModeling::CalculationSummary } 62 | it "should have one column per 4 filings" do 63 | subject.num_value_columns.should be_within(2).of((@filings.length/4).floor) 64 | end 65 | end 66 | context "when a quarterly disclosure is requested" do 67 | subject { @filings.disclosures(/Disclosure Components Of Total Comprehensive I/, :quarterly) } 68 | it { should be_a FinModeling::CalculationSummary } 69 | it "should have one column per filing" do 70 | #subject.num_value_columns.should be_within(3).of(@filings.length) 71 | pending "this is returning more than expected. not sure why..." 72 | end 73 | end 74 | context "when no period modifier is given (and the disclosure is yearly)" do 75 | subject { @filings.disclosures(/Disclosure Components Of Gross And Net Intangible Asset Balances/) } 76 | it { should be_a FinModeling::CalculationSummary } 77 | it "should have one column per 4 filings" do 78 | subject.num_value_columns.should be_within(2).of((@filings.length/4).floor) 79 | end 80 | end 81 | end 82 | 83 | describe ".choose_forecasting_policy" do 84 | context "when one or two filings" do 85 | let(:filings) { FinModeling::CompanyFilings.new(@filings.last(2)) } 86 | it "should raise an error because there aren't enough filings" do 87 | expect { filings.choose_forecasting_policy(e_ror=0.10) }.to raise_error 88 | end 89 | end 90 | context "when three or more filings" do 91 | let(:filings) { FinModeling::CompanyFilings.new(@filings.last(3)) } 92 | subject { filings.choose_forecasting_policy(e_ror=0.10) } 93 | it { should be_a FinModeling::LinearTrendForecastingPolicy } 94 | end 95 | end 96 | 97 | describe ".forecasts" do 98 | let(:policy) { @filings.choose_forecasting_policy(e_ror=0.10) } 99 | let(:num_quarters) { 3 } 100 | subject { @filings.forecasts(policy, num_quarters) } 101 | it { should be_a FinModeling::Forecasts } 102 | its(:reformulated_income_statements) { should have(num_quarters).items } 103 | its(:reformulated_balance_sheets) { should have(num_quarters).items } 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/company_spec.rb: -------------------------------------------------------------------------------- 1 | # company_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::Company do 6 | describe "initialize" do 7 | let(:entity) { SecQuery::Entity.find("aapl", {:relationships=>false, :transactions=>false, :filings=>true}) } 8 | subject { FinModeling::Company.new(entity) } 9 | it { should be_a FinModeling::Company } 10 | end 11 | 12 | describe "find" do 13 | context "when given a valid stock ticker" do 14 | subject { FinModeling::Company.find("aapl") } 15 | it { should be_a FinModeling::Company } 16 | end 17 | context "when given a bogus stock ticker" do 18 | subject { FinModeling::Company.find("bogus symbol") } 19 | it { should be_nil } 20 | end 21 | end 22 | 23 | let(:company) { FinModeling::Company.find("aapl") } 24 | 25 | describe "name" do 26 | subject { company.name } 27 | it { should == "APPLE INC" } 28 | end 29 | 30 | describe "annual_reports" do 31 | subject { company.annual_reports } 32 | it { should be_a FinModeling::CompanyFilings } 33 | specify { subject.all?{ |report| report.term == "10-K" }.should be_true } 34 | end 35 | 36 | describe "quarterly_reports" do 37 | subject { company.quarterly_reports } 38 | it { should be_a FinModeling::CompanyFilings } 39 | specify { subject.all?{ |report| report.term == "10-Q" }.should be_true } 40 | end 41 | 42 | describe "filings_since_date" do 43 | let(:start_date) { Time.parse("1994-01-01") } 44 | subject { company.filings_since_date(start_date) } 45 | it { should be_a FinModeling::CompanyFilings } 46 | 47 | context "when start date is 1994" do 48 | let(:start_date) { Time.parse("1994-01-01") } 49 | subject { company.filings_since_date(start_date) } 50 | it { should have_at_least(11).items } 51 | end 52 | context "when start date is 2010" do 53 | let(:start_date) { Time.parse("2010-01-01") } 54 | subject { company.filings_since_date(start_date) } 55 | it { should have_at_least( 9).items } 56 | end 57 | context "when start date is 2011" do 58 | let(:start_date) { Time.parse("2011-01-01") } 59 | subject { company.filings_since_date(start_date) } 60 | it { should have_at_least( 5).items } 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/comprehensive_income_statement_calculation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe FinModeling::ComprehensiveIncomeStatementCalculation do 4 | pending "not yet working..." 5 | # before(:all) do 6 | # xray_2012_q2_rpt = "http://www.sec.gov/Archives/edgar/data/1288776/000119312511282235/0001193125-11-282235-index.htm" 7 | # filing_q2 = FinModeling::AnnualReportFiling.download xray_2012_q2_rpt 8 | # @prev_ci_stmt = filing_q2.comprehensive_income_statement 9 | # 10 | # xray_2012_q3_rpt = "http://www.sec.gov/Archives/edgar/data/818479/000081847912000023/0000818479-12-000023-index.htm" 11 | # filing = FinModeling::AnnualReportFiling.download xray_2012_q3_rpt 12 | # @ci_stmt = filing.comprehensive_income_statement 13 | # @period = @ci_stmt.periods.last 14 | # end 15 | # 16 | # describe ".comprehensive_income_calculation" do 17 | # subject { @ci_stmt.comprehensive_income_calculation } 18 | # it { should be_a FinModeling::ComprehensiveIncomeCalculation } 19 | # its(:label) { should match /comprehensive.*income/i } 20 | # end 21 | # 22 | # describe ".is_valid?" do 23 | # subject { @ci_stmt.is_valid? } 24 | # it { should == (@ci_stmt.comprehensive_income_calculation.has_net_income_item? || @ci_stmt.comprehensive_income_calculation.has_revenue_item?) } 25 | # end 26 | # 27 | # describe ".reformulated" do 28 | # subject { @ci_stmt.reformulated(@period, ci_calc=nil) } 29 | # it { should be_a FinModeling::ReformulatedIncomeStatement } 30 | # end 31 | # 32 | # describe ".latest_quarterly_reformulated" do 33 | # context "has invalid total operating revenues or invalid cost of revenue" do 34 | # subject{ @ci_stmt.latest_quarterly_reformulated(@ci_stmt, @prev_ci_stmt, @prev_ci_calc) } 35 | # it { should be_nil } 36 | # end 37 | # context "has valid total operating revenues and valid cost of revenue" do 38 | # pending "need example..." 39 | # #it { should be_a FinModeling::ReformulatedIncomeStatement } 40 | # end 41 | # end 42 | # 43 | # describe ".write_constructor" do 44 | # before(:all) do 45 | # file_name = "/tmp/finmodeling-ci-stmt.rb" 46 | # item_name = "@ci_stmt" 47 | # file = File.open(file_name, "w") 48 | # @ci_stmt.write_constructor(file, item_name) 49 | # file.close 50 | # 51 | # eval(File.read(file_name)) 52 | # @loaded_cis = eval(item_name) 53 | # end 54 | # 55 | # subject { @loaded_cis } 56 | # it { should have_the_same_periods_as(@ci_stmt) } 57 | # #it { should have_the_same_reformulated_last_total(:net_financing_income).as(@ci_stmt) } 58 | # end 59 | # 60 | end 61 | 62 | -------------------------------------------------------------------------------- /spec/comprehensive_income_statement_item_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe FinModeling::ComprehensiveIncomeStatementItem do 4 | 5 | describe "new" do 6 | subject { FinModeling::ComprehensiveIncomeStatementItem.new("Comprehensive Income Net Of Tax Attributable To Noncontrolling Interest") } 7 | it { should be_a FinModeling::ComprehensiveIncomeStatementItem } 8 | end 9 | 10 | describe "train" do 11 | let(:item) { FinModeling::ComprehensiveIncomeStatementItem.new("Comprehensive Income Net Of Tax Attributable To Noncontrolling Interest") } 12 | it "trains the classifier that this ComprehensiveIncomeStatementItem is of the given type" do 13 | item.train(:ooci_nci) 14 | end 15 | end 16 | 17 | describe "classification_estimates" do 18 | let(:item) { FinModeling::ComprehensiveIncomeStatementItem.new("Comprehensive Income Net Of Tax Attributable To Noncontrolling Interest") } 19 | subject { item.classification_estimates } 20 | its(:keys) { should == FinModeling::ComprehensiveIncomeStatementItem::TYPES } 21 | end 22 | 23 | describe "classify" do 24 | let(:cisi) { FinModeling::ComprehensiveIncomeStatementItem.new("Comprehensive Income Net Of Tax Attributable To Noncontrolling Interest") } 25 | subject { cisi.classify } 26 | it "returns the ComprehensiveIncomeStatementItem type with the highest probability estimate" do 27 | estimates = cisi.classification_estimates 28 | estimates[subject].should be_within(0.1).of(estimates.values.max) 29 | end 30 | end 31 | 32 | describe "load_vectors_and_train" do 33 | # the before(:all) clause calls load_vectors_and_train already 34 | # we can just focus, here, on its effects 35 | 36 | it "classifies >95% correctly" do # FIXME: add more vectors to tighten this up 37 | num_items = 0 38 | errors = [] 39 | FinModeling::ComprehensiveIncomeStatementItem::TRAINING_VECTORS.each do |vector| 40 | num_items += 1 41 | cisi = FinModeling::ComprehensiveIncomeStatementItem.new(vector[:item_string]) 42 | if cisi.classify != vector[:klass] 43 | errors.push({ :cisi=>cisi.to_s, :expected=>vector[:klass], :got=>cisi.classify }) 44 | end 45 | end 46 | 47 | pct_errors = errors.length.to_f / num_items 48 | if pct_errors > 0.05 49 | puts "errors: " + errors.inspect 50 | end 51 | pct_errors.should be < 0.05 52 | 53 | end 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /spec/debt_cost_of_capital_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe FinModeling::DebtCostOfCapital do 4 | describe "#calculate" do 5 | context "if given the after tax cost" do 6 | let(:r) { FinModeling::Rate.new(0.05) } 7 | subject { FinModeling::DebtCostOfCapital.calculate(:after_tax_cost => r) } 8 | it { should be_a FinModeling::Rate } 9 | its(:value) { should be_within(0.1).of(r.value) } 10 | end 11 | context "if given the after tax cost and marginal tax rate" do 12 | let(:r) { FinModeling::Rate.new(0.05) } 13 | let(:t) { FinModeling::Rate.new(0.35) } 14 | subject { FinModeling::DebtCostOfCapital.calculate(:before_tax_cost => r, :marginal_tax_rate => t) } 15 | it { should be_a FinModeling::Rate } 16 | its(:value) { should be_within(0.1).of(r.value * (1.0 - t.value)) } 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/equity_change_calculation_spec.rb: -------------------------------------------------------------------------------- 1 | # equity_change_calculation_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::EquityChangeCalculation do 6 | before(:all) do 7 | deere_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/315189/000110465910063219/0001104659-10-063219-index.htm" 8 | @filing = FinModeling::AnnualReportFiling.download(deere_2011_annual_rpt) 9 | @ses_period = @filing.shareholder_equity_statement.periods.last 10 | 11 | @equity_changes = @filing.shareholder_equity_statement.equity_change_calculation 12 | 13 | bs_period_initial = @filing.balance_sheet.periods[-2] 14 | bs_period_final = @filing.balance_sheet.periods[-1] 15 | 16 | @equity_plus_minority_int_initial = @filing.balance_sheet.reformulated(bs_period_initial).common_shareholders_equity.total + 17 | @filing.balance_sheet.reformulated(bs_period_initial).minority_interest .total 18 | @equity_plus_minority_int_final = @filing.balance_sheet.reformulated(bs_period_final) .common_shareholders_equity.total + 19 | @filing.balance_sheet.reformulated(bs_period_final) .minority_interest .total 20 | end 21 | 22 | describe ".summary" do 23 | subject{ @equity_changes.summary(:period => @ses_period) } 24 | 25 | it { should be_an_instance_of FinModeling::CalculationSummary } 26 | 27 | describe ".total" do 28 | subject{ @equity_changes.summary(:period => @ses_period).total } 29 | it { should be_within(1.0).of(@equity_plus_minority_int_final - @equity_plus_minority_int_initial) } 30 | end 31 | end 32 | end 33 | 34 | -------------------------------------------------------------------------------- /spec/equity_change_item_spec.rb: -------------------------------------------------------------------------------- 1 | # equity_change_item_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::EquityChangeItem do 6 | 7 | describe "new" do 8 | subject { FinModeling::EquityChangeItem.new("Depreciation and amortization of property and equipment") } 9 | it { should be_a FinModeling::EquityChangeItem } 10 | end 11 | 12 | describe "train" do 13 | let(:item) { FinModeling::EquityChangeItem.new("Depreciation and amortization of property and equipment") } 14 | it "trains the classifier that this EquityChangeItem is of the given type" do 15 | item.train(:oci) 16 | end 17 | end 18 | 19 | describe "classification_estimates" do 20 | let(:item) { FinModeling::EquityChangeItem.new("Depreciation and amortization of property and equipment") } 21 | subject { item.classification_estimates } 22 | its(:keys) { should == FinModeling::EquityChangeItem::TYPES } 23 | end 24 | 25 | describe "classify" do 26 | let(:eci) { FinModeling::EquityChangeItem.new("Depreciation and amortization of property and equipment") } 27 | subject { eci.classify } 28 | it "returns the EquityChangeItem type with the highest probability estimate" do 29 | estimates = eci.classification_estimates 30 | estimates[subject].should be_within(0.1).of(estimates.values.max) 31 | end 32 | end 33 | 34 | describe "load_vectors_and_train" do 35 | # the before(:all) clause calls load_vectors_and_train already 36 | # we can just focus, here, on its effects 37 | 38 | it "classifies >95% correctly" do # FIXME: add more vectors to tighten this up 39 | num_items = 0 40 | errors = [] 41 | FinModeling::EquityChangeItem::TRAINING_VECTORS.each do |vector| 42 | num_items += 1 43 | eci = FinModeling::EquityChangeItem.new(vector[:item_string]) 44 | if eci.classify != vector[:klass] 45 | errors.push({ :eci=>eci.to_s, :expected=>vector[:klass], :got=>eci.classify }) 46 | end 47 | end 48 | 49 | pct_errors = errors.length.to_f / num_items 50 | if pct_errors > 0.05 51 | puts "errors: " + errors.inspect 52 | end 53 | pct_errors.should be < 0.05 54 | 55 | end 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /spec/factory_spec.rb: -------------------------------------------------------------------------------- 1 | # factory_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::Factory do 6 | describe "BalanceSheetCalculation" do 7 | subject { FinModeling::Factory.BalanceSheetCalculation } 8 | 9 | it { should be_a FinModeling::BalanceSheetCalculation } 10 | end 11 | 12 | describe "incomeStatementCalculation" do 13 | subject { FinModeling::Factory.IncomeStatementCalculation } 14 | 15 | it { should be_a FinModeling::IncomeStatementCalculation } 16 | end 17 | end 18 | 19 | -------------------------------------------------------------------------------- /spec/forecasts_spec.rb: -------------------------------------------------------------------------------- 1 | # company_filings_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::Forecasts do 6 | before (:all) do 7 | @company = FinModeling::Company.find("aapl") 8 | @filings = FinModeling::CompanyFilings.new(@company.filings_since_date(Time.parse("2010-10-01"))) 9 | @forecasts = @filings.forecasts(@filings.choose_forecasting_policy(e_ror=0.10), num_quarters=3) 10 | end 11 | 12 | describe "balance_sheet_analyses" do 13 | subject { @forecasts.balance_sheet_analyses(@filings) } 14 | it { should be_a FinModeling::CalculationSummary } 15 | end 16 | 17 | describe "income_statement_analyses" do 18 | subject { @forecasts.income_statement_analyses(@filings, e_ror=0.10) } 19 | it { should be_a FinModeling::CalculationSummary } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/income_statement_analyses_spec.rb: -------------------------------------------------------------------------------- 1 | # income_statement_analyses_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::IncomeStatementAnalyses do 6 | before(:all) do 7 | @summary = FinModeling::CalculationSummary.new 8 | @summary.title = "Title 123" 9 | @summary.rows = [ ] 10 | @summary.rows << FinModeling::CalculationRow.new(:key => "Revenue Growth", :type => :oa, :vals => [ 4]) 11 | @summary.rows << FinModeling::CalculationRow.new(:key => "Sales / NOA", :type => :oa, :vals => [ 4]) 12 | @summary.rows << FinModeling::CalculationRow.new(:key => "Operating PM", :type => :oa, :vals => [ 4]) 13 | @summary.rows << FinModeling::CalculationRow.new(:key => "FI / NFA", :type => :oa, :vals => [ 4]) 14 | @summary.rows << FinModeling::CalculationRow.new(:key => "Row", :type => :fa, :vals => [109]) 15 | @summary.rows << FinModeling::CalculationRow.new(:key => "Row", :type => :oa, :vals => [ 93]) 16 | @summary.rows << FinModeling::CalculationRow.new(:key => "Row", :type => :fa, :vals => [ 1]) 17 | end 18 | 19 | describe ".new" do 20 | subject { FinModeling::IncomeStatementAnalyses.new(@summary) } 21 | 22 | it { should be_a_kind_of FinModeling::CalculationSummary } 23 | its(:title) { should == @summary.title } 24 | its(:rows) { should == @summary.rows } 25 | its(:header_row) { should == @summary.header_row } 26 | its(:rows) { should == @summary.rows } 27 | its(:num_value_columns) { should == @summary.num_value_columns } 28 | its(:key_width) { should == @summary.key_width } 29 | its(:val_width) { should == @summary.val_width } 30 | its(:max_decimals) { should == @summary.max_decimals } 31 | its(:totals_row_enabled) { should be_false } 32 | end 33 | 34 | let(:isa) { FinModeling::IncomeStatementAnalyses.new(@summary) } 35 | 36 | describe ".print_regressions" do 37 | subject { isa } 38 | 39 | it { should respond_to(:print_regressions) } 40 | end 41 | 42 | describe ".revenue_growth_row" do 43 | subject { isa.revenue_growth_row } 44 | it { should be_a FinModeling::CalculationRow } 45 | its(:key) { should == "Revenue Growth" } 46 | end 47 | 48 | describe ".operating_pm_row" do 49 | subject { isa.operating_pm_row } 50 | it { should be_a FinModeling::CalculationRow } 51 | its(:key) { should == "Operating PM" } 52 | end 53 | 54 | describe ".sales_over_noa_row" do 55 | subject { isa.sales_over_noa_row } 56 | it { should be_a FinModeling::CalculationRow } 57 | its(:key) { should == "Sales / NOA" } 58 | end 59 | 60 | describe ".fi_over_nfa_row" do 61 | subject { isa.fi_over_nfa_row } 62 | it { should be_a FinModeling::CalculationRow } 63 | its(:key) { should == "FI / NFA" } 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/income_statement_calculation_spec.rb: -------------------------------------------------------------------------------- 1 | # income_statement_calculation_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::IncomeStatementCalculation do 6 | before(:all) do 7 | google_2010_q3_rpt = "http://www.sec.gov/Archives/edgar/data/1288776/000119312511282235/0001193125-11-282235-index.htm" 8 | filing_q3 = FinModeling::AnnualReportFiling.download google_2010_q3_rpt 9 | @prev_inc_stmt = filing_q3.income_statement 10 | 11 | google_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/1288776/000119312512025336/0001193125-12-025336-index.htm" 12 | filing = FinModeling::AnnualReportFiling.download google_2011_annual_rpt 13 | @inc_stmt = filing.income_statement 14 | @period = @inc_stmt.periods.last 15 | end 16 | 17 | describe ".net_income_calculation" do 18 | subject { @inc_stmt.net_income_calculation } 19 | it { should be_a FinModeling::NetIncomeCalculation } 20 | its(:label) { should match /net.*income/i } 21 | end 22 | 23 | describe ".is_valid?" do 24 | subject { @inc_stmt.is_valid? } 25 | it { should == (@inc_stmt.net_income_calculation.has_tax_item? && @inc_stmt.net_income_calculation.has_revenue_item?) } 26 | end 27 | 28 | describe ".reformulated" do 29 | subject { @inc_stmt.reformulated(@period, ci_calc=nil) } 30 | it { should be_a FinModeling::ReformulatedIncomeStatement } 31 | end 32 | 33 | describe ".latest_quarterly_reformulated" do 34 | subject{ @inc_stmt.latest_quarterly_reformulated(ci_calc=nil, @prev_inc_stmt, prev_ci_calc=nil) } 35 | it { should be_a FinModeling::ReformulatedIncomeStatement } 36 | end 37 | 38 | describe ".write_constructor" do 39 | before(:all) do 40 | file_name = "/tmp/finmodeling-inc-stmt.rb" 41 | item_name = "@inc_stmt" 42 | file = File.open(file_name, "w") 43 | @inc_stmt.write_constructor(file, item_name) 44 | file.close 45 | 46 | eval(File.read(file_name)) 47 | @loaded_is = eval(item_name) 48 | end 49 | 50 | subject { @loaded_is } 51 | it { should have_the_same_periods_as(@inc_stmt) } 52 | it { should have_the_same_reformulated_last_total(:net_financing_income).as(@inc_stmt) } 53 | end 54 | 55 | end 56 | 57 | -------------------------------------------------------------------------------- /spec/income_statement_item_spec.rb: -------------------------------------------------------------------------------- 1 | # period_array_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::IncomeStatementItem do 6 | let(:isi) { FinModeling::IncomeStatementItem.new("Cost of Goods Sold") } 7 | 8 | describe "new" do 9 | subject { isi } 10 | it { should be_a FinModeling::IncomeStatementItem } 11 | end 12 | 13 | describe "train" do 14 | it "trains the classifier that this ISI is of the given type" do 15 | isi.train(:tax) 16 | end 17 | end 18 | 19 | describe "classification_estimates" do 20 | subject { isi.classification_estimates } 21 | it { should be_a Hash } 22 | its(:keys) { should == FinModeling::IncomeStatementItem::TYPES } 23 | end 24 | 25 | describe "classify" do 26 | subject { isi.classify } 27 | it "returns the ISI type with the highest probability estimate" do 28 | estimates = isi.classification_estimates 29 | estimates[subject].should be_within(0.1).of(estimates.values.max) 30 | end 31 | end 32 | 33 | describe "load_vectors_and_train" do 34 | context "tax" do 35 | subject { FinModeling::IncomeStatementItem.new("provision for income tax").classify } 36 | it { should == :tax } 37 | end 38 | 39 | context "or" do 40 | subject { FinModeling::IncomeStatementItem.new("software licensing revenues net").classify } 41 | it { should == :or } 42 | end 43 | 44 | it "classifies >95% correctly" do 45 | num_items = 0 46 | errors = [] 47 | FinModeling::IncomeStatementItem::TRAINING_VECTORS.each do |vector| 48 | num_items += 1 49 | isi = FinModeling::IncomeStatementItem.new(vector[:item_string]) 50 | if isi.classify != vector[:klass] 51 | errors.push({ :isi=>isi.to_s, :expected=>vector[:klass], :got=>isi.classify }) 52 | end 53 | end 54 | 55 | pct_errors = errors.length.to_f / num_items 56 | if pct_errors > 0.05 57 | puts "errors: " + errors.inspect 58 | end 59 | pct_errors.should be < 0.05 60 | 61 | end 62 | end 63 | 64 | describe "tokenize" do 65 | subject { FinModeling::IncomeStatementItem.new("Cost of Goods Sold").tokenize } 66 | it "returns an array of downcased 1-word, 2-word, and 3-word tokens" do 67 | expected_tokens = ["^", "cost", "of", "goods", "sold", "$"] 68 | expected_tokens += ["^ cost", "cost of", "of goods", "goods sold", "sold $"] 69 | expected_tokens += ["^ cost of", "cost of goods", "of goods sold", "goods sold $"] 70 | subject.sort.should == expected_tokens.sort 71 | end 72 | end 73 | 74 | end 75 | -------------------------------------------------------------------------------- /spec/liabs_and_equity_calculation_spec.rb: -------------------------------------------------------------------------------- 1 | # liabs_and_equity_calculation_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::LiabsAndEquityCalculation do 6 | before(:all) do 7 | google_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/1288776/000119312512025336/0001193125-12-025336-index.htm" 8 | filing = FinModeling::AnnualReportFiling.download google_2011_annual_rpt 9 | @bal_sheet = filing.balance_sheet 10 | @period = @bal_sheet.periods.last 11 | @lse = @bal_sheet.liabs_and_equity_calculation 12 | end 13 | 14 | describe "summary" do 15 | subject { @lse.summary(:period=>@period) } 16 | it { should be_a FinModeling::CalculationSummary } 17 | end 18 | 19 | describe ".has_equity_item" do 20 | pending "Find a test case..." 21 | end 22 | end 23 | 24 | -------------------------------------------------------------------------------- /spec/liabs_and_equity_item_spec.rb: -------------------------------------------------------------------------------- 1 | # liabs_and_equity_item_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::LiabsAndEquityItem do 6 | 7 | describe ".new" do 8 | subject { FinModeling::LiabsAndEquityItem.new("Accounts Payable Current") } 9 | it { should be_a FinModeling::LiabsAndEquityItem } 10 | end 11 | 12 | describe ".train" do 13 | it "trains the classifier that this LiabsAndEquityItem is of the given type" do 14 | FinModeling::LiabsAndEquityItem.new("Accounts Payable Current").train(:ol) 15 | end 16 | end 17 | 18 | describe ".classification_estimates" do 19 | subject { FinModeling::LiabsAndEquityItem.new("Accounts Payable Current").classification_estimates } 20 | 21 | it { should be_a Hash } 22 | specify { subject.keys.sort == FinModeling::LiabsAndEquityItem::TYPES.sort } 23 | end 24 | 25 | describe ".classify" do 26 | let(:laei) { FinModeling::LiabsAndEquityItem.new("Accounts Payable Current") } 27 | subject { laei.classify } 28 | it "returns the LiabsAndEquityItem type with the highest probability estimate" do 29 | estimates = laei.classification_estimates 30 | estimates[subject].should be_within(0.1).of(estimates.values.max) 31 | end 32 | end 33 | 34 | describe ".load_vectors_and_train" do 35 | # the before(:all) clause calls load_vectors_and_train already 36 | # we can just focus, here, on its effects 37 | 38 | it "classifies >95% correctly" do 39 | num_items = 0 40 | errors = [] 41 | FinModeling::LiabsAndEquityItem::TRAINING_VECTORS.each do |vector| 42 | num_items += 1 43 | laei = FinModeling::LiabsAndEquityItem.new(vector[:item_string]) 44 | if laei.classify != vector[:klass] 45 | errors.push({ :laei=>laei.to_s, :expected=>vector[:klass], :got=>laei.classify }) 46 | end 47 | end 48 | 49 | pct_errors = errors.length.to_f / num_items 50 | if pct_errors > 0.05 51 | puts "errors: " + errors.inspect 52 | end 53 | pct_errors.should be < 0.05 54 | 55 | end 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /spec/linear_trend_forecasting_policy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe FinModeling::LinearTrendForecastingPolicy do 4 | before (:all) do 5 | @vals = { :revenue_estimator => FinModeling::TimeSeriesEstimator.new(0.04, 0.0), 6 | :sales_pm_estimator => FinModeling::TimeSeriesEstimator.new(0.20, 0.0), 7 | :fi_over_nfa_estimator => FinModeling::TimeSeriesEstimator.new(0.01, 0.0), 8 | :sales_over_noa_estimator => FinModeling::TimeSeriesEstimator.new(2.00, 0.0) } 9 | end 10 | 11 | let(:policy) { FinModeling::LinearTrendForecastingPolicy.new(@vals) } 12 | let(:date) { Date.today } 13 | 14 | describe ".revenue_on" do 15 | subject { policy.revenue_on(date) } 16 | it { should be_a Float } 17 | it { should be_within(0.01).of(@vals[:revenue_estimator].a) } 18 | end 19 | 20 | describe ".sales_pm_on" do 21 | subject { policy.sales_pm_on(date) } 22 | it { should be_a Float } 23 | it { should be_within(0.01).of(@vals[:sales_pm_estimator].a) } 24 | end 25 | 26 | describe ".fi_over_nfa_on" do 27 | subject { policy.fi_over_nfa_on(date) } 28 | it { should be_a Float } 29 | it { should be_within(0.01).of(@vals[:fi_over_nfa_estimator].a) } 30 | end 31 | 32 | describe ".sales_over_noa_on" do 33 | subject { policy.sales_over_noa_on(date) } 34 | it { should be_a Float } 35 | it { should be_within(0.01).of(@vals[:sales_over_noa_estimator].a) } 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/matchers/custom_matchers.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :be_in do |expected| 2 | match do |actual| 3 | expected.include?(actual) 4 | end 5 | 6 | description { "be one of #{expected}" } 7 | failure_message_for_should { |actual| "expected one of #{expected} but got '#{actual}'" } 8 | failure_message_for_should_not { |actual| "expected other one of #{expected} but got '#{actual}'" } 9 | end 10 | 11 | RSpec::Matchers.define :have_the_same_periods_as do |expected| 12 | match do |actual| 13 | str1 = actual .periods.map{|x| x.to_pretty_s}.join(',') 14 | str2 = expected.periods.map{|x| x.to_pretty_s}.join(',') 15 | str1 == str2 16 | end 17 | end 18 | 19 | RSpec::Matchers.define :have_a_plausible_total do 20 | match do |actual| 21 | actual.total.abs >= 1.0 22 | end 23 | 24 | description { "have a plausible total" } 25 | failure_message_for_should { |actual| "expected that #{actual} would have a total with an absolute value > 1.0" } 26 | failure_message_for_should_not { |actual| "expected that #{actual} would not have a total with an absolute value > 1.0" } 27 | end 28 | 29 | RSpec::Matchers.define :have_the_same_last_total_as do |expected| 30 | match do |actual| 31 | period = actual.periods.last 32 | val1 = actual .summary(:period=>period).total 33 | val2 = expected.summary(:period=>period).total 34 | (val1 - val2).abs <= 0.1 35 | end 36 | 37 | description { "have the same last reformulated total as #{expected}" } 38 | failure_message_for_should { |actual| "expected that #{actual} would have the same last total as #{expected}" } 39 | failure_message_for_should_not { |actual| "expected that #{actual} would not have the same last total as #{expected}" } 40 | end 41 | 42 | RSpec::Matchers.define :have_the_same_last_total do |calc| 43 | match do |actual| 44 | a = actual .send(calc) 45 | e = expected.send(calc) 46 | 47 | period = a.periods.last 48 | val1 = a.summary(:period=>period).total 49 | val2 = e.summary(:period=>period).total 50 | (val1 - val2).abs <= 0.1 51 | end 52 | chain :as do |expected| 53 | @expected = expected 54 | end 55 | 56 | description { "have #{calc} with the same last total as #{@expected}" } 57 | failure_message_for_should { |actual| "expected that #{actual}'s #{calc} would have the same last total as #{@expected}" } 58 | failure_message_for_should_not { |actual| "expected that #{actual}'s #{calc} would not have the same last total as #{@expected}" } 59 | end 60 | 61 | RSpec::Matchers.define :have_the_same_reformulated_last_total do |calc| 62 | match do |actual| 63 | period = actual.periods.last 64 | 65 | params = (actual.is_a? FinModeling::IncomeStatementCalculation) ? [period, ci_calc=nil] : [period] 66 | 67 | val1 = actual .reformulated(*params).send(calc).total 68 | val2 = expected.reformulated(*params).send(calc).total 69 | (val1 - val2).abs <= 0.1 70 | end 71 | chain :as do |expected| 72 | @expected = expected 73 | end 74 | 75 | description { "have the same last reformulated total as #{@expected}" } 76 | failure_message_for_should { |actual| "expected that #{actual} would have the same last reformulated total as #{@expected}" } 77 | failure_message_for_should_not { |actual| "expected that #{actual} would not have the same last reformulated total as #{@expected}" } 78 | end 79 | 80 | -------------------------------------------------------------------------------- /spec/mocks/calculation.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | module Mocks 3 | class Calculation 4 | attr_accessor :label 5 | def initialize 6 | @label = "Dummy" 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/mocks/income_statement_analyses.rb: -------------------------------------------------------------------------------- 1 | @item = FinModeling::MultiColumnCalculationSummary.new 2 | @item.title = "" 3 | @item.num_value_columns = 9 4 | @item.key_width = 18 5 | @item.val_width = 12 6 | @item.max_decimals = 4 7 | @item.totals_row_enabled = false 8 | args = { } 9 | args[:key] = "" 10 | args[:vals] = ["Unknown...", "2010-03-31", "2010-06-30", "2010-09-30", "2010-12-31", "2011-03-31", "2011-06-30", "2011-09-30", "2011-12-31"] 11 | @item_header_row = FinModeling::MultiColumnCalculationSummaryHeaderRow.new(args) 12 | @item.header_row = @item_header_row 13 | args = { } 14 | args[:key] = "Revenue (000's)" 15 | args[:type] = "" 16 | args[:vals] = [0, 1596960.0, 1601379.0, 1601203.0, 1525109.0, 1214357.0, 1229024.0, 1216665.0, 1324153.0] 17 | @item_row0 = FinModeling::MultiColumnCalculationSummaryRow.new(args) 18 | args = { } 19 | args[:key] = "Core OI (000's)" 20 | args[:type] = "" 21 | args[:vals] = [0, 171660.0, 117788.0, 173458.0, 230022.0, 146258.0, 133437.0, 126070.0, 178193.0] 22 | @item_row1 = FinModeling::MultiColumnCalculationSummaryRow.new(args) 23 | args = { } 24 | args[:key] = "OI (000's)" 25 | args[:type] = "" 26 | args[:vals] = [0, 168792.0, 111254.0, 169715.0, 205495.0, 139384.0, 133283.0, 127839.0, 167579.0] 27 | @item_row2 = FinModeling::MultiColumnCalculationSummaryRow.new(args) 28 | args = { } 29 | args[:key] = "FI (000's)" 30 | args[:type] = "" 31 | args[:vals] = [0, 141399.0, 102067.0, 226416.0, 106525.0, 83608.0, 103689.0, 165452.0, 127993.0] 32 | @item_row3 = FinModeling::MultiColumnCalculationSummaryRow.new(args) 33 | args = { } 34 | args[:key] = "NI (000's)" 35 | args[:type] = "" 36 | args[:vals] = [0, 310191.0, 213321.0, 396131.0, 312020.0, 222992.0, 236972.0, 293291.0, 295572.0] 37 | @item_row4 = FinModeling::MultiColumnCalculationSummaryRow.new(args) 38 | args = { } 39 | args[:key] = "Gross Margin" 40 | args[:type] = "" 41 | args[:vals] = [0, 0.5576708245666767, 0.5736661964469373, 0.5748484108510913, 0.6343297429888618, 0.689167188890911, 0.6980083383237431, 0.7047042530195247, 0.7018796166304045] 42 | @item_row5 = FinModeling::MultiColumnCalculationSummaryRow.new(args) 43 | args = { } 44 | args[:key] = "Sales PM" 45 | args[:type] = "" 46 | args[:vals] = [0, 0.10749148381925659, 0.07355385577055774, 0.10832951849328286, 0.15082361326305202, 0.12044085882487604, 0.10857147622829172, 0.10361968988998616, 0.13457104277224763] 47 | @item_row6 = FinModeling::MultiColumnCalculationSummaryRow.new(args) 48 | args = { } 49 | args[:key] = "Operating PM" 50 | args[:type] = "" 51 | args[:vals] = [0, 0.10569569682396554, 0.06947374731403372, 0.10599208844849779, 0.13474099228317452, 0.1147804558297107, 0.10844613286640456, 0.10507337681284495, 0.12655546602243095] 52 | @item_row7 = FinModeling::MultiColumnCalculationSummaryRow.new(args) 53 | args = { } 54 | args[:key] = "FI / Sales" 55 | args[:type] = "" 56 | args[:vals] = [0, 0.0885427311892596, 0.06373706661571059, 0.14140377578608085, 0.06984766334734108, 0.06884923461552081, 0.08436702619314188, 0.13598804929869768, 0.09666043123415496] 57 | @item_row8 = FinModeling::MultiColumnCalculationSummaryRow.new(args) 58 | args = { } 59 | args[:key] = "NI / Sales" 60 | args[:type] = "" 61 | args[:vals] = [0, 0.19423842801322513, 0.1332108139297443, 0.24739586423457863, 0.2045886556305156, 0.1836296904452315, 0.19281315905954644, 0.24106142611154263, 0.22321589725658592] 62 | @item_row9 = FinModeling::MultiColumnCalculationSummaryRow.new(args) 63 | args = { } 64 | args[:key] = "Sales / NOA" 65 | args[:type] = "" 66 | args[:vals] = [0, 0, 0.31589471931674, 0.3027171516963245, 0.2812087090658527, 0.22230803971150923, 0.21556306903197936, 0.2058721704664517, 0.22777909563225565] 67 | @item_row10 = FinModeling::MultiColumnCalculationSummaryRow.new(args) 68 | args = { } 69 | args[:key] = "FI / NFA" 70 | args[:type] = "" 71 | args[:vals] = [0, 0, 0.01388443269851155, 0.03307811869055056, 0.01601019419285102, 0.011782960277534123, 0.014556844204896556, 0.0241571697495845, 0.019256398729732317] 72 | @item_row11 = FinModeling::MultiColumnCalculationSummaryRow.new(args) 73 | args = { } 74 | args[:key] = "Revenue Growth" 75 | args[:type] = "" 76 | args[:vals] = [0, 0, 0.01114526118072745, -0.00043596613617347124, -0.17565789513442365, -0.6030968787992619, 0.04933275376563051, -0.03930454685490847, 0.39916742999669985] 77 | @item_row12 = FinModeling::MultiColumnCalculationSummaryRow.new(args) 78 | args = { } 79 | args[:key] = "Core OI Growth" 80 | args[:type] = "" 81 | args[:vals] = [0, 0, -0.7792359004477285, 3.644010853978843, 2.0641568409990847, -0.8406048886193428, -0.30787317437309647, -0.20172411964684556, 2.9464409948934227] 82 | @item_row13 = FinModeling::MultiColumnCalculationSummaryRow.new(args) 83 | args = { } 84 | args[:key] = "OI Growth" 85 | args[:type] = "" 86 | args[:vals] = [0, 0, -0.8121268899268889, 4.341206465523186, 1.136062554145436, -0.7928481118131626, -0.16434540325239466, -0.1524844979682214, 1.926767066125802] 87 | @item_row14 = FinModeling::MultiColumnCalculationSummaryRow.new(args) 88 | args = { } 89 | args[:key] = "ReOI (000's)" 90 | args[:type] = "" 91 | args[:vals] = [0, 0, -10648.0, 41106.0, 73629.0, 9489.0, -3819.0, -15854.0, 26232.0] 92 | @item_row15 = FinModeling::MultiColumnCalculationSummaryRow.new(args) 93 | @item.rows = [@item_row0,@item_row1,@item_row2,@item_row3,@item_row4,@item_row5,@item_row6,@item_row7,@item_row8,@item_row9,@item_row10,@item_row11,@item_row12,@item_row13,@item_row14,@item_row15] 94 | -------------------------------------------------------------------------------- /spec/mocks/sec_query.rb: -------------------------------------------------------------------------------- 1 | module FinModeling 2 | module Mocks 3 | class Filing_10K 4 | attr_accessor :term, :date, :link 5 | def initialize 6 | @term="10-K" 7 | @date="1994-01-26T00:00:00-05:00" 8 | @link="http://www.sec.gov/Archives/edgar/data/320193/000119312512023398/0001193125-12-023398-index.htm" 9 | end 10 | end 11 | 12 | class Filing_10Q 13 | attr_accessor :term, :date, :link 14 | def initialize 15 | @term="10-Q" 16 | @date="1995-01-26T00:00:00-05:00" 17 | @link="http://www.sec.gov/Archives/edgar/data/1288776/000119312511282235/0001193125-11-282235-index.htm" 18 | end 19 | end 20 | 21 | class Entity 22 | attr_accessor :name, :filings 23 | def initialize 24 | @name = "Apple Inc" 25 | @filings = [] 26 | @filings.push Mocks::Filing_10K.new 27 | @filings.push Mocks::Filing_10Q.new 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/net_income_calculation_spec.rb: -------------------------------------------------------------------------------- 1 | # net_income_calculation_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::NetIncomeCalculation do 6 | before(:all) do 7 | google_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/1288776/000119312512025336/0001193125-12-025336-index.htm" 8 | filing = FinModeling::AnnualReportFiling.download google_2011_annual_rpt 9 | @period = filing.income_statement.periods.last 10 | @ni = filing.income_statement.net_income_calculation 11 | end 12 | 13 | describe ".summary" do 14 | subject { @ni.summary(:period=>@period) } 15 | it { should be_a FinModeling::CalculationSummary } 16 | it "should tag each row with an Income Statement Type" do 17 | subject.rows.first.type.should be_in(FinModeling::IncomeStatementItem::TYPES) # FIXME: seems weak. 18 | end 19 | end 20 | 21 | describe ".has_revenue_item?" do 22 | pending "Find a test case..." 23 | end 24 | 25 | describe ".has_tax_item?" do 26 | pending "Find a test case..." 27 | end 28 | end 29 | 30 | -------------------------------------------------------------------------------- /spec/period_array.rb: -------------------------------------------------------------------------------- 1 | # period_array_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::PeriodArray do 6 | before(:all) do 7 | @t_now = Date.parse('2012-01-01') 8 | @t_3mo_ago = Date.parse('2011-09-01') 9 | @t_6mo_ago = Date.parse('2011-06-01') 10 | @t_0mo_ago = Date.parse('2011-03-01') 11 | @t_1yr_ago = Date.parse('2011-01-01') 12 | 13 | @arr = FinModeling::PeriodArray.new 14 | @arr.push Xbrlware::Context::Period.new({"start_date"=>@t_1yr_ago, "end_date"=>@t_now}) # 1 yr 15 | @arr.push Xbrlware::Context::Period.new({"start_date"=>@t_1yr_ago, "end_date"=>@t_3mo_ago}) # 9 mo 16 | @arr.push Xbrlware::Context::Period.new({"start_date"=>@t_1yr_ago, "end_date"=>@t_6mo_ago}) # 6 mo 17 | @arr.push Xbrlware::Context::Period.new({"start_date"=>@t_3mo_ago, "end_date"=>@t_now}) # 3 mo 18 | end 19 | 20 | describe "yearly" do 21 | subject { @arr.yearly } 22 | it { should be_an_instance_of FinModeling::PeriodArray } 23 | it "returns only annual periods" do 24 | subject.first.to_pretty_s.should == @arr[0].to_pretty_s 25 | end 26 | end 27 | 28 | describe "threequarterly" do 29 | subject { @arr.threequarterly } 30 | it { should be_an_instance_of FinModeling::PeriodArray } 31 | it "returns only three-quarter periods" do 32 | subject.first.to_pretty_s.should == @arr[1].to_pretty_s 33 | end 34 | end 35 | 36 | describe "halfyearly" do 37 | subject { @arr.halfyearly } 38 | it { should be_an_instance_of FinModeling::PeriodArray } 39 | it "returns only two-quarter periods" do 40 | subject.first.to_pretty_s.should == @arr[2].to_pretty_s 41 | end 42 | end 43 | 44 | describe "quarterly" do 45 | subject { @arr.quarterly } 46 | it { should be_an_instance_of FinModeling::PeriodArray } 47 | it "returns only quarterly periods" do 48 | subject.first.to_pretty_s.should == @arr[3].to_pretty_s 49 | end 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /spec/quarterly_report_filing_spec.rb: -------------------------------------------------------------------------------- 1 | # quarterly_report_filing_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::QuarterlyReportFiling do 6 | before(:all) do 7 | company = FinModeling::Company.new(FinModeling::Mocks::Entity.new) 8 | filing_url = company.quarterly_reports.last.link 9 | FinModeling::Config::disable_caching 10 | @filing = FinModeling::QuarterlyReportFiling.download(filing_url) 11 | FinModeling::Config::enable_caching 12 | end 13 | 14 | subject { @filing } 15 | its(:balance_sheet) { should be_a FinModeling::BalanceSheetCalculation } 16 | its(:income_statement) { should be_a FinModeling::IncomeStatementCalculation } 17 | its(:cash_flow_statement) { should be_a FinModeling::CashFlowStatementCalculation } 18 | 19 | context "when the report doesn't have a statement of shareholders' equity" do 20 | its(:has_a_shareholder_equity_statement?) { should be_false } 21 | #its(:is_valid?) { should == [@filing.income_statement, 22 | # @filing.balance_sheet, 23 | # @filing.cash_flow_statement].all?{|x| x.is_valid?} } # FIXME: this is failing, but I'm not sure how I want it to work. 24 | end 25 | 26 | context "after write_constructor()ing it to a file and then eval()ing the results" do 27 | before(:all) do 28 | file_name = "/tmp/finmodeling-quarterly-rpt.rb" 29 | schema_version_item_name = "@schema_version" 30 | item_name = "@quarterly_rpt" 31 | file = File.open(file_name, "w") 32 | @filing.write_constructor(file, item_name) 33 | file.close 34 | 35 | eval(File.read(file_name)) 36 | 37 | @schema_version = eval(schema_version_item_name) 38 | @loaded_filing = eval(item_name) 39 | end 40 | 41 | it "writes itself to a file, and saves a schema version of 1.3" do 42 | @schema_version.should be == 1.3 43 | end 44 | 45 | subject { @loaded_filing } 46 | its(:balance_sheet) { should have_the_same_periods_as(@filing.balance_sheet) } 47 | its(:balance_sheet) { should have_the_same_reformulated_last_total(:net_operating_assets).as(@filing.balance_sheet) } 48 | its(:income_statement) { should have_the_same_reformulated_last_total(:net_financing_income).as(@filing.income_statement) } 49 | its(:cash_flow_statement) { should have_the_same_last_total(:cash_change_calculation).as(@filing.cash_flow_statement) } 50 | its(:disclosures) { should have_the_same_last_total(:first).as(@filing.disclosures) } 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/rate_spec.rb: -------------------------------------------------------------------------------- 1 | # rate_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::Rate do 6 | describe ".annualize" do 7 | let(:val) { 0.2 } 8 | context "when annualizing from a quarter to a year" do 9 | subject { FinModeling::Rate.new(val).annualize(from_days=365.0/4.0, to_days=365.0) } 10 | it { should be_a_kind_of Float } 11 | it { should be_within(0.0001).of( (val+1.0)**(4.00) - 1.0 ) } 12 | end 13 | context "when annualizing from a year to a quarter" do 14 | subject { FinModeling::Rate.new(val).annualize(from_days=365.0, to_days=365.0/4.0) } 15 | it { should be_a_kind_of Float } 16 | it { should be_within(0.0001).of( (val+1.0)**(0.25) - 1.0 ) } 17 | end 18 | end 19 | 20 | describe ".yearly_to_quarterly" do 21 | let(:val) { 0.2 } 22 | subject { FinModeling::Rate.new(val).yearly_to_quarterly } 23 | it { should be_a_kind_of Float } 24 | it { should be_within(0.0001).of( FinModeling::Rate.new(val).annualize(from_days=365.0, to_days=365.0/4.0) ) } 25 | end 26 | 27 | describe ".quarterly_to_yearly" do 28 | let(:val) { 0.2 } 29 | subject { FinModeling::Rate.new(val).quarterly_to_yearly } 30 | it { should be_a_kind_of Float } 31 | it { should be_within(0.0001).of( FinModeling::Rate.new(val).annualize(from_days=365.0/4.0, to_days=365.0) ) } 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/ratio_spec.rb: -------------------------------------------------------------------------------- 1 | # rate_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::Ratio do 6 | describe ".annualize" do 7 | let(:val) { 0.2 } 8 | context "when annualizing from a quarter to a year" do 9 | subject { FinModeling::Ratio.new(val).annualize(from_days=365.0/4.0, to_days=365.0) } 10 | it { should be_a_kind_of Float } 11 | it { should be_within(0.0001).of(val * 4.0) } 12 | end 13 | context "when annualizing from a year to a quarter" do 14 | subject { FinModeling::Ratio.new(val).annualize(from_days=365.0, to_days=365.0/4.0) } 15 | it { should be_a_kind_of Float } 16 | it { should be_within(0.0001).of(val / 4.0) } 17 | end 18 | end 19 | 20 | describe ".yearly_to_quarterly" do 21 | let(:val) { 0.2 } 22 | subject { FinModeling::Ratio.new(val).yearly_to_quarterly } 23 | it { should be_a_kind_of Float } 24 | it { should be_within(0.0001).of( FinModeling::Ratio.new(val).annualize(from_days=365.0, to_days=365.0/4.0) ) } 25 | end 26 | 27 | describe ".quarterly_to_yearly" do 28 | let(:val) { 0.2 } 29 | subject { FinModeling::Ratio.new(val).quarterly_to_yearly } 30 | it { should be_a_kind_of Float } 31 | it { should be_within(0.0001).of( FinModeling::Ratio.new(val).annualize(from_days=365.0/4.0, to_days=365.0) ) } 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/reformulated_shareholder_equity_statement_spec.rb: -------------------------------------------------------------------------------- 1 | # reformulated_income_statement_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::ReformulatedShareholderEquityStatement do 6 | before(:all) do 7 | deere_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/315189/000110465910063219/0001104659-10-063219-index.htm" 8 | filing = FinModeling::AnnualReportFiling.download deere_2011_annual_rpt 9 | stmt = filing.shareholder_equity_statement 10 | period = stmt.periods.last 11 | 12 | @equity_chg = stmt.equity_change_calculation.summary(:period => period) 13 | @re_ses = stmt.reformulated(period) 14 | end 15 | 16 | describe "transactions_with_shareholders" do 17 | subject { @re_ses.transactions_with_shareholders } 18 | it { should be_a FinModeling::CalculationSummary } 19 | its(:total) { should be_within(0.1).of( @equity_chg.filter_by_type(:share_issue ).total + 20 | @equity_chg.filter_by_type(:minority_int ).total + 21 | @equity_chg.filter_by_type(:share_repurch).total + 22 | @equity_chg.filter_by_type(:common_div ).total) } 23 | end 24 | 25 | describe "comprehensive_income" do 26 | subject { @re_ses.comprehensive_income } 27 | it { should be_a FinModeling::CalculationSummary } 28 | its(:total) { should be_within(0.1).of( @equity_chg.filter_by_type(:net_income ).total + 29 | @equity_chg.filter_by_type(:oci ).total + 30 | @equity_chg.filter_by_type(:preferred_div).total) } 31 | end 32 | 33 | describe "analysis" do 34 | subject { @re_ses.analysis } 35 | 36 | it { should be_a FinModeling::CalculationSummary } 37 | it "contains the expected rows" do 38 | expected_keys = [ "Tx w Shareholders ($MM)", "CI ($MM)" ] 39 | subject.rows.map{ |row| row.key }.should == expected_keys 40 | end 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /spec/shareholder_equity_statement_calculation_spec.rb: -------------------------------------------------------------------------------- 1 | # shareholder_equity_statement_calculation_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe FinModeling::ShareholderEquityStatementCalculation do 6 | before(:all) do 7 | deere_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/315189/000110465910063219/0001104659-10-063219-index.htm" 8 | filing = FinModeling::AnnualReportFiling.download deere_2011_annual_rpt 9 | @stmt = filing.shareholder_equity_statement 10 | @period = @stmt.periods.last 11 | end 12 | 13 | describe ".equity_change_calculation" do 14 | subject { @stmt.equity_change_calculation } 15 | it { should be_a FinModeling::EquityChangeCalculation } 16 | its(:label) { should match /(stock|share)holder.*equity/i } 17 | 18 | #let(:right_side_sum) { @stmt.liabs_and_equity_calculation.leaf_items_sum(:period=>@period) } 19 | #specify { subject.leaf_items_sum(:period=>@period).should be_within(1.0).of(right_side_sum) } 20 | 21 | it "should have the same last total as the balance sheet''s cse" do 22 | pending 23 | end 24 | end 25 | 26 | describe ".is_valid?" do 27 | context "always... ?" do 28 | it "returns true" do 29 | @stmt.is_valid?.should be_true 30 | end 31 | end 32 | end 33 | 34 | describe ".reformulated" do 35 | subject { @stmt.reformulated(@period) } 36 | it { should be_a FinModeling::ReformulatedShareholderEquityStatement } 37 | end 38 | 39 | describe ".write_constructor" do 40 | before(:all) do 41 | file_name = "/tmp/finmodeling-shareholder-equity-stmt.rb" 42 | item_name = "@stmt" 43 | file = File.open(file_name, "w") 44 | @stmt.write_constructor(file, item_name) 45 | file.close 46 | 47 | eval(File.read(file_name)) 48 | @loaded_stmt = eval(item_name) 49 | end 50 | 51 | context "after write_constructor()ing it to a file and then eval()ing the results" do 52 | subject { @loaded_stmt } 53 | it { should have_the_same_periods_as @stmt } 54 | #it { should have_the_same_reformulated_last_total(:net_operating_assets).as(@stmt) } 55 | end 56 | end 57 | 58 | end 59 | 60 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | 3 | $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "../lib"))) 4 | require 'finmodeling' 5 | 6 | require 'mocks/sec_query' 7 | require 'mocks/calculation' 8 | require 'matchers/custom_matchers' 9 | -------------------------------------------------------------------------------- /spec/string_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | # string_helpers_spec.rb 2 | 3 | require 'spec_helper' 4 | 5 | describe String do 6 | let(:s) { "asdfasdf" } 7 | 8 | describe "matches_any_regex?" do 9 | context "if no regexes are provided" do 10 | let(:regexes) { [] } 11 | subject { s.matches_any_regex?(regexes) } 12 | it { should be_false } 13 | end 14 | context "if the string does not match any of the regexes" do 15 | let(:regexes) { [/\d/, /[A-Z]/] } 16 | subject { s.matches_any_regex?(regexes) } 17 | it { should be_false } 18 | end 19 | context "if the string matches one or more of the regexes" do 20 | let(:regexes) { [/sdf/, /ddd/, /af+/] } 21 | subject { s.matches_any_regex?(regexes) } 22 | it { should be_true } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/time_series_estimator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe FinModeling::TimeSeriesEstimator do 4 | 5 | describe ".new" do 6 | let(:a) { 1.0 } 7 | let(:b) { 0.2 } 8 | subject { FinModeling::TimeSeriesEstimator.new(a, b) } 9 | 10 | it { should be_a FinModeling::TimeSeriesEstimator } 11 | its(:a) { should be_within(0.01).of(a) } 12 | its(:b) { should be_within(0.01).of(b) } 13 | end 14 | 15 | describe ".estimate_on" do 16 | let(:a) { 1.0 } 17 | let(:b) { 0.2 } 18 | let(:estimator) { FinModeling::TimeSeriesEstimator.new(a, b) } 19 | 20 | context "when predicting today's outcome" do 21 | let(:date) { Date.today } 22 | subject { estimator.estimate_on(date) } 23 | 24 | it { should be_a Float } 25 | it { should be_within(0.01).of(a) } 26 | end 27 | 28 | context "when predicting any other day" do 29 | let(:date) { Date.parse("2014-01-01") } 30 | let(:num_days) { date - Date.today } 31 | subject { estimator.estimate_on(date) } 32 | 33 | it { should be_a Float } 34 | it { should be_within(0.01).of(a + (b*num_days)) } 35 | end 36 | end 37 | 38 | describe "#from_time_series" do 39 | let(:ys) { [ 10, 20 ] } 40 | let(:dates) { [ (Date.today - 1), (Date.today) ] } 41 | subject { FinModeling::TimeSeriesEstimator.from_time_series(dates, ys) } 42 | let(:expected_a) { 20 } 43 | let(:expected_b) { 20-10 } 44 | 45 | it { should be_a FinModeling::TimeSeriesEstimator } 46 | its(:a) { should be_within(0.01).of(expected_a) } 47 | its(:b) { should be_within(0.01).of(expected_b) } 48 | end 49 | 50 | describe "#from_const" do 51 | let(:y) { [ 10 ] } 52 | subject { FinModeling::TimeSeriesEstimator.from_const(y) } 53 | let(:expected_a) { 10 } 54 | let(:expected_b) { 0 } 55 | 56 | it { should be_a FinModeling::TimeSeriesEstimator } 57 | its(:a) { should be_within(0.01).of(expected_a) } 58 | its(:b) { should be_within(0.01).of(expected_b) } 59 | end 60 | 61 | end 62 | -------------------------------------------------------------------------------- /spec/trailing_avg_forecasting_policy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe FinModeling::TrailingAvgForecastingPolicy do 4 | before (:all) do 5 | @vals = { :revenue_estimator => FinModeling::TimeSeriesEstimator.new(0.04, 0.0), 6 | :sales_pm_estimator => FinModeling::TimeSeriesEstimator.new(0.20, 0.0), 7 | :fi_over_nfa_estimator => FinModeling::TimeSeriesEstimator.new(0.01, 0.0), 8 | :sales_over_noa_estimator => FinModeling::TimeSeriesEstimator.new(2.00, 0.0) } 9 | end 10 | 11 | let(:policy) { FinModeling::TrailingAvgForecastingPolicy.new(@vals) } 12 | let(:date) { Date.today } 13 | 14 | describe ".revenue_on" do 15 | subject { policy.revenue_on(date) } 16 | it { should be_a Float } 17 | it { should be_within(0.01).of(@vals[:revenue_estimator].a) } 18 | end 19 | 20 | describe ".sales_pm_on" do 21 | subject { policy.sales_pm_on(date) } 22 | it { should be_a Float } 23 | it { should be_within(0.01).of(@vals[:sales_pm_estimator].a) } 24 | end 25 | 26 | describe ".fi_over_nfa_on" do 27 | subject { policy.fi_over_nfa_on(date) } 28 | it { should be_a Float } 29 | it { should be_within(0.01).of(@vals[:fi_over_nfa_estimator].a) } 30 | end 31 | 32 | describe ".sales_over_noa_on" do 33 | subject { policy.sales_over_noa_on(date) } 34 | it { should be_a Float } 35 | it { should be_within(0.01).of(@vals[:sales_over_noa_estimator].a) } 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/weighted_avg_cost_of_capital_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe FinModeling::WeightedAvgCostOfCapital do 4 | let(:equity_market_val) { 2.2*1000*1000*1000 } 5 | let(:debt_market_val) { 997.0*1000*1000 } 6 | let(:cost_of_equity) { FinModeling::Rate.new(0.0087) } 7 | let(:after_tax_cost_of_debt) { FinModeling::Rate.new(0.0031) } 8 | 9 | describe '.new' do 10 | subject { FinModeling::WeightedAvgCostOfCapital.new(equity_market_val, debt_market_val, cost_of_equity, after_tax_cost_of_debt) } 11 | 12 | it { should be_a FinModeling::WeightedAvgCostOfCapital } 13 | end 14 | 15 | describe '.rate' do 16 | let(:wacc) { FinModeling::WeightedAvgCostOfCapital.new(equity_market_val, debt_market_val, cost_of_equity, after_tax_cost_of_debt) } 17 | subject { wacc.rate } 18 | 19 | let(:total_val) { equity_market_val + debt_market_val } 20 | let(:e_weight) { equity_market_val / total_val } 21 | let(:d_weight) { debt_market_val / total_val } 22 | let(:expected_wacc) { (e_weight * cost_of_equity.value) + (d_weight * after_tax_cost_of_debt.value) } 23 | its(:value) { should be_within(1.0).of(expected_wacc) } 24 | end 25 | 26 | describe '.summary' do 27 | let(:wacc) { FinModeling::WeightedAvgCostOfCapital.new(equity_market_val, debt_market_val, cost_of_equity, after_tax_cost_of_debt) } 28 | subject { wacc.summary } 29 | 30 | it { should be_a FinModeling::CalculationSummary } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /tools/create_balance_sheet_training_vectors.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH << "." 4 | 5 | require 'finmodeling' 6 | 7 | def get_args 8 | if ARGV.length != 2 9 | puts "usage #{__FILE__} " 10 | exit 11 | end 12 | 13 | args = { :stock_symbol => nil, :filing_url => nil, :assets_or_liabs => nil } 14 | arg = ARGV[0] 15 | if arg =~ /http/ 16 | args[:filing_url] = arg 17 | else 18 | args[:stock_symbol] = arg.downcase 19 | end 20 | case ARGV[1] 21 | when "assets" 22 | args[:assets_or_liabs] = "assets" 23 | when "liabilities" 24 | args[:assets_or_liabs] = "liabilities" 25 | else 26 | puts "usage #{__FILE__} " 27 | exit 28 | end 29 | 30 | return args 31 | end 32 | 33 | def get_company_filing_url(stock_symbol) 34 | company = FinModeling::Company.find(stock_symbol) 35 | raise RuntimeError.new("couldn't find company") if company.nil? 36 | raise RuntimeError.new("company has no annual reports") if company.annual_reports.length == 0 37 | filing_url = company.annual_reports.last.link 38 | 39 | return filing_url 40 | end 41 | 42 | def get_filing(filing_url) 43 | filing = FinModeling::AnnualReportFiling.download(filing_url) 44 | return filing 45 | end 46 | 47 | def print_assets(filing) 48 | period = filing.balance_sheet.assets_calculation.periods.last 49 | items = filing.balance_sheet.assets_calculation.leaf_items(period) 50 | items.each { |item| puts item.pretty_name } 51 | puts 52 | end 53 | 54 | def print_liabilities(filing) 55 | period = filing.balance_sheet.liabs_and_equity_calculation.periods.last 56 | items = filing.balance_sheet.liabs_and_equity_calculation.leaf_items(period) 57 | items.each { |item| puts item.pretty_name } 58 | puts 59 | end 60 | 61 | args = get_args 62 | filing_url = args[:filing_url] || get_company_filing_url(args[:stock_symbol]) 63 | filing = get_filing(filing_url) 64 | print_assets(filing) if args[:assets_or_liabs] == "assets" 65 | print_liabilities(filing) if args[:assets_or_liabs] == "liabilities" 66 | -------------------------------------------------------------------------------- /tools/create_cash_change_training_vectors.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH << "." 4 | 5 | require 'finmodeling' 6 | 7 | def get_args 8 | if ARGV.length != 1 9 | puts "usage #{__FILE__} " 10 | exit 11 | end 12 | 13 | args = { :stock_symbol => nil, :filing_url => nil } 14 | arg = ARGV[0] 15 | if arg =~ /http/ 16 | args[:filing_url] = arg 17 | else 18 | args[:stock_symbol] = arg.downcase 19 | end 20 | 21 | return args 22 | end 23 | 24 | def get_company_filing_url(stock_symbol) 25 | company = FinModeling::Company.find(stock_symbol) 26 | raise RuntimeError.new("couldn't find company") if company.nil? 27 | raise RuntimeError.new("company has no annual reports") if company.annual_reports.length == 0 28 | filing_url = company.annual_reports.last.link 29 | 30 | return filing_url 31 | end 32 | 33 | def get_filing(filing_url) 34 | filing = FinModeling::AnnualReportFiling.download(filing_url) 35 | return filing 36 | end 37 | 38 | def print_items(filing) 39 | items = filing.cash_flow_statement.cash_change_calculation.leaf_items 40 | items.each do |item| 41 | puts " { :cci_type=>:c, :item_string=>\"#{item.pretty_name}\" }," 42 | end 43 | end 44 | 45 | args = get_args 46 | filing_url = args[:filing_url] || get_company_filing_url(args[:stock_symbol]) 47 | filing = get_filing(filing_url) 48 | print_items(filing) 49 | -------------------------------------------------------------------------------- /tools/create_credit_debit_training_vectors.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH << "." 4 | 5 | require 'finmodeling' 6 | 7 | def get_args 8 | if ARGV.length != 1 9 | puts "usage #{__FILE__} " 10 | exit 11 | end 12 | 13 | args = { :stock_symbol => nil, :filing_url => nil } 14 | arg = ARGV[0] 15 | if arg =~ /http/ 16 | args[:filing_url] = arg 17 | else 18 | args[:stock_symbol] = arg.downcase 19 | end 20 | 21 | return args 22 | end 23 | 24 | def get_company_filing_url(stock_symbol) 25 | company = FinModeling::Company.find(stock_symbol) 26 | raise RuntimeError.new("couldn't find company") if company.nil? 27 | raise RuntimeError.new("company has no annual reports") if company.annual_reports.length == 0 28 | filing_url = company.annual_reports.last.link 29 | 30 | return filing_url 31 | end 32 | 33 | def get_filing(filing_url) 34 | filing = FinModeling::AnnualReportFiling.download(filing_url) 35 | return filing 36 | end 37 | 38 | def print_items(filing) 39 | items = filing.balance_sheet.leaf_items 40 | items += filing.income_statement.leaf_items 41 | items.each do |item| 42 | if !item.def.nil? 43 | puts "{ :balance_defn=>:#{item.def["xbrli:balance"]}, :item_string=>\"#{item.pretty_name}\" }," 44 | end 45 | end 46 | end 47 | 48 | args = get_args 49 | filing_url = args[:filing_url] || get_company_filing_url(args[:stock_symbol]) 50 | filing = get_filing(filing_url) 51 | print_items(filing) 52 | -------------------------------------------------------------------------------- /tools/create_equity_change_training_vectors.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH << "." 4 | 5 | require 'finmodeling' 6 | 7 | def get_args 8 | if ARGV.length != 1 9 | puts "usage #{__FILE__} " 10 | exit 11 | end 12 | 13 | args = { :stock_symbol => nil, :filing_url => nil } 14 | arg = ARGV[0] 15 | if arg =~ /http/ 16 | args[:filing_url] = arg 17 | else 18 | args[:stock_symbol] = arg.downcase 19 | end 20 | 21 | return args 22 | end 23 | 24 | def get_company_filing_url(stock_symbol) 25 | company = FinModeling::Company.find(stock_symbol) 26 | raise RuntimeError.new("couldn't find company") if company.nil? 27 | raise RuntimeError.new("company has no annual reports") if company.annual_reports.length == 0 28 | filing_url = company.annual_reports.last.link 29 | 30 | return filing_url 31 | end 32 | 33 | def get_filing(filing_url) 34 | filing = FinModeling::AnnualReportFiling.download(filing_url) 35 | return filing 36 | end 37 | 38 | def print_items(filing) 39 | return if !filing.has_a_shareholder_equity_statement? 40 | items = filing.shareholder_equity_statement.equity_change_calculation.leaf_items 41 | items.each do |item| 42 | puts " { :eci_type=>:c, :item_string=>\"#{item.pretty_name}\" }," 43 | end 44 | end 45 | 46 | args = get_args 47 | filing_url = args[:filing_url] || get_company_filing_url(args[:stock_symbol]) 48 | filing = get_filing(filing_url) 49 | print_items(filing) 50 | -------------------------------------------------------------------------------- /tools/create_income_statement_training_vectors.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH << "." 4 | 5 | require 'finmodeling' 6 | 7 | def get_args 8 | if ARGV.length != 1 9 | puts "usage #{__FILE__} " 10 | exit 11 | end 12 | 13 | args = { :stock_symbol => nil, :filing_url => nil } 14 | arg = ARGV[0] 15 | if arg =~ /http/ 16 | args[:filing_url] = arg 17 | else 18 | args[:stock_symbol] = arg.downcase 19 | end 20 | 21 | return args 22 | end 23 | 24 | def get_company_filing_url(stock_symbol) 25 | company = FinModeling::Company.find(stock_symbol) 26 | raise RuntimeError.new("couldn't find company") if company.nil? 27 | raise RuntimeError.new("company has no annual reports") if company.annual_reports.length == 0 28 | filing_url = company.annual_reports.last.link 29 | 30 | return filing_url 31 | end 32 | 33 | def get_filing(filing_url) 34 | filing = FinModeling::AnnualReportFiling.download(filing_url) 35 | return filing 36 | end 37 | 38 | def print_income_statement(filing) 39 | period = filing.income_statement.net_income_calculation.periods.yearly.last 40 | items = filing.income_statement.net_income_calculation.leaf_items(period) 41 | items.each { |item| puts item.pretty_name } 42 | puts 43 | end 44 | 45 | args = get_args 46 | filing_url = args[:filing_url] || get_company_filing_url(args[:stock_symbol]) 47 | filing = get_filing(filing_url) 48 | print_income_statement(filing) #if filing.is_valid? 49 | -------------------------------------------------------------------------------- /tools/time_specs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for i in `/bin/ls spec/*spec.rb`; do 4 | /usr/bin/time -f "%E" rspec -c -fd -I. -Ispec $i > /dev/null 2>/tmp/jbl_time.txt 5 | ELAPSED=`cat /tmp/jbl_time.txt`; echo "$ELAPSED $i" 6 | rm /tmp/jbl_time.txt 7 | done 8 | --------------------------------------------------------------------------------