finmodeling 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +3 -0
- data/Gemfile +10 -0
- data/README.md +292 -0
- data/Rakefile +6 -0
- data/TODO.txt +36 -0
- data/examples/dump_report.rb +33 -0
- data/examples/lists/nasdaq-mid-to-mega-tech-symbols.txt +226 -0
- data/examples/show_report.rb +218 -0
- data/examples/show_reports.rb +77 -0
- data/finmodeling.gemspec +31 -0
- data/lib/finmodeling/annual_report_filing.rb +104 -0
- data/lib/finmodeling/array_with_stats.rb +22 -0
- data/lib/finmodeling/assets_calculation.rb +36 -0
- data/lib/finmodeling/assets_item.rb +14 -0
- data/lib/finmodeling/assets_item_vectors.rb +638 -0
- data/lib/finmodeling/balance_sheet_analyses.rb +33 -0
- data/lib/finmodeling/balance_sheet_calculation.rb +68 -0
- data/lib/finmodeling/calculation_summary.rb +148 -0
- data/lib/finmodeling/can_cache_classifications.rb +36 -0
- data/lib/finmodeling/can_cache_summaries.rb +16 -0
- data/lib/finmodeling/can_classify_rows.rb +54 -0
- data/lib/finmodeling/cash_change_calculation.rb +67 -0
- data/lib/finmodeling/cash_change_item.rb +14 -0
- data/lib/finmodeling/cash_change_item_vectors.rb +241 -0
- data/lib/finmodeling/cash_flow_statement_calculation.rb +85 -0
- data/lib/finmodeling/classifiers.rb +11 -0
- data/lib/finmodeling/company.rb +102 -0
- data/lib/finmodeling/company_filing.rb +64 -0
- data/lib/finmodeling/company_filing_calculation.rb +75 -0
- data/lib/finmodeling/company_filings.rb +100 -0
- data/lib/finmodeling/config.rb +37 -0
- data/lib/finmodeling/constant_forecasting_policy.rb +23 -0
- data/lib/finmodeling/factory.rb +27 -0
- data/lib/finmodeling/float_helpers.rb +17 -0
- data/lib/finmodeling/forecasts.rb +48 -0
- data/lib/finmodeling/generic_forecasting_policy.rb +19 -0
- data/lib/finmodeling/has_string_classifer.rb +96 -0
- data/lib/finmodeling/income_statement_analyses.rb +74 -0
- data/lib/finmodeling/income_statement_calculation.rb +71 -0
- data/lib/finmodeling/income_statement_item.rb +14 -0
- data/lib/finmodeling/income_statement_item_vectors.rb +654 -0
- data/lib/finmodeling/liabs_and_equity_calculation.rb +36 -0
- data/lib/finmodeling/liabs_and_equity_item.rb +14 -0
- data/lib/finmodeling/liabs_and_equity_item_vectors.rb +1936 -0
- data/lib/finmodeling/net_income_calculation.rb +41 -0
- data/lib/finmodeling/paths.rb +5 -0
- data/lib/finmodeling/period_array.rb +24 -0
- data/lib/finmodeling/quarterly_report_filing.rb +23 -0
- data/lib/finmodeling/rate.rb +20 -0
- data/lib/finmodeling/ratio.rb +20 -0
- data/lib/finmodeling/reformulated_balance_sheet.rb +176 -0
- data/lib/finmodeling/reformulated_cash_flow_statement.rb +140 -0
- data/lib/finmodeling/reformulated_income_statement.rb +436 -0
- data/lib/finmodeling/string_helpers.rb +26 -0
- data/lib/finmodeling/version.rb +3 -0
- data/lib/finmodeling.rb +70 -0
- data/spec/annual_report_filing_spec.rb +68 -0
- data/spec/assets_calculation_spec.rb +21 -0
- data/spec/assets_item_spec.rb +66 -0
- data/spec/balance_sheet_analyses_spec.rb +43 -0
- data/spec/balance_sheet_calculation_spec.rb +91 -0
- data/spec/calculation_summary_spec.rb +63 -0
- data/spec/can_classify_rows_spec.rb +86 -0
- data/spec/cash_change_calculation_spec.rb +56 -0
- data/spec/cash_change_item_spec.rb +66 -0
- data/spec/cash_flow_statement_calculation_spec.rb +108 -0
- data/spec/company_filing_calculation_spec.rb +74 -0
- data/spec/company_filing_spec.rb +30 -0
- data/spec/company_filings_spec.rb +55 -0
- data/spec/company_spec.rb +73 -0
- data/spec/constant_forecasting_policy_spec.rb +37 -0
- data/spec/factory_spec.rb +18 -0
- data/spec/forecasts_spec.rb +21 -0
- data/spec/generic_forecasting_policy_spec.rb +33 -0
- data/spec/income_statement_analyses_spec.rb +63 -0
- data/spec/income_statement_calculation_spec.rb +88 -0
- data/spec/income_statement_item_spec.rb +86 -0
- data/spec/liabs_and_equity_calculation_spec.rb +20 -0
- data/spec/liabs_and_equity_item_spec.rb +66 -0
- data/spec/mocks/calculation.rb +10 -0
- data/spec/mocks/income_statement_analyses.rb +93 -0
- data/spec/mocks/sec_query.rb +31 -0
- data/spec/net_income_calculation_spec.rb +23 -0
- data/spec/period_array.rb +52 -0
- data/spec/quarterly_report_filing_spec.rb +69 -0
- data/spec/rate_spec.rb +33 -0
- data/spec/ratio_spec.rb +33 -0
- data/spec/reformulated_balance_sheet_spec.rb +146 -0
- data/spec/reformulated_cash_flow_statement_spec.rb +174 -0
- data/spec/reformulated_income_statement_spec.rb +293 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/string_helpers_spec.rb +23 -0
- data/tools/create_balance_sheet_training_vectors.rb +65 -0
- data/tools/create_cash_change_training_vectors.rb +48 -0
- data/tools/create_credit_debit_training_vectors.rb +51 -0
- data/tools/create_income_statement_training_vectors.rb +48 -0
- metadata +289 -0
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
it "takes a string and returns a new AssetsItem" do
|
|
13
|
+
ai = FinModeling::AssetsItem.new("Property Plant And Equipment Net")
|
|
14
|
+
ai.should be_an_instance_of FinModeling::AssetsItem
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe "train" do
|
|
19
|
+
it "trains the classifier that this AssetsItem is of the given type" do
|
|
20
|
+
FinModeling::AssetsItem.new("Property Plant and Equipment Net").train(:oa)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "classification_estimates" do
|
|
25
|
+
it "returns a hash with the confidence in each AssetsItem type" do
|
|
26
|
+
ai = FinModeling::AssetsItem.new("Property Plant And Equipment Net")
|
|
27
|
+
|
|
28
|
+
FinModeling::AssetsItem::TYPES.each do |klass|
|
|
29
|
+
ai.classification_estimates.keys.include?(klass).should be_true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe "classify" do
|
|
35
|
+
it "returns the AssetsItem type with the highest probability estimate" do
|
|
36
|
+
ai = FinModeling::AssetsItem.new("Property Plant And Equipment Net")
|
|
37
|
+
estimates = ai.classification_estimates
|
|
38
|
+
estimates[ai.classify].should be_within(0.1).of(estimates.values.max)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe "load_vectors_and_train" do
|
|
43
|
+
# the before(:all) clause calls load_vectors_and_train already
|
|
44
|
+
# we can just focus, here, on its effects
|
|
45
|
+
|
|
46
|
+
it "classifies >95% correctly" do
|
|
47
|
+
num_items = 0
|
|
48
|
+
errors = []
|
|
49
|
+
FinModeling::AssetsItem::TRAINING_VECTORS.each do |vector|
|
|
50
|
+
num_items += 1
|
|
51
|
+
ai = FinModeling::AssetsItem.new(vector[:item_string])
|
|
52
|
+
if ai.classify != vector[:klass]
|
|
53
|
+
errors.push({ :ai=>ai.to_s, :expected=>vector[:klass], :got=>ai.classify })
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
pct_errors = errors.length.to_f / num_items
|
|
58
|
+
if pct_errors > 0.05
|
|
59
|
+
puts "errors: " + errors.inspect
|
|
60
|
+
end
|
|
61
|
+
pct_errors.should be < 0.05
|
|
62
|
+
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
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_extras" do
|
|
32
|
+
subject { FinModeling::BalanceSheetAnalyses.new(@summary) }
|
|
33
|
+
|
|
34
|
+
it { should respond_to(:print_extras) }
|
|
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
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
it "returns an AssetsCalculation" do
|
|
15
|
+
@balance_sheet.assets_calculation.should be_an_instance_of FinModeling::AssetsCalculation
|
|
16
|
+
end
|
|
17
|
+
it "returns the root node of the assets calculation" do
|
|
18
|
+
@balance_sheet.assets_calculation.label.downcase.should match /asset/
|
|
19
|
+
end
|
|
20
|
+
it "sums to the same value as do the liabilities and equity" do
|
|
21
|
+
left_sum = @balance_sheet.assets_calculation.leaf_items_sum(:period=>@period)
|
|
22
|
+
right_sum = @balance_sheet.liabs_and_equity_calculation.leaf_items_sum(:period=>@period)
|
|
23
|
+
left_sum.should be_within(1.0).of(right_sum)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe "liabs_and_equity_calculation" do
|
|
28
|
+
it "returns a LiabsAndEquityCalculation" do
|
|
29
|
+
@balance_sheet.liabs_and_equity_calculation.should be_an_instance_of FinModeling::LiabsAndEquityCalculation
|
|
30
|
+
end
|
|
31
|
+
it "returns the root node of the liability & shareholders' equity calculation" do
|
|
32
|
+
@balance_sheet.liabs_and_equity_calculation.label.downcase.should match /liab.*equity/
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe "is_valid?" do
|
|
37
|
+
it "returns false if none of the asset leaf nodes contains the term 'cash'" do
|
|
38
|
+
#ea_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/712515/000119312511149262/0001193125-11-149262-index.htm"
|
|
39
|
+
#filing = FinModeling::AnnualReportFiling.download ea_2011_annual_rpt
|
|
40
|
+
#filing.balance_sheet.is_valid?.should be_false
|
|
41
|
+
pending
|
|
42
|
+
end
|
|
43
|
+
it "returns false if none of the liability/equity net income leaf nodes contains the term 'equity'" do
|
|
44
|
+
#ea_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/712515/000119312511149262/0001193125-11-149262-index.htm"
|
|
45
|
+
#filing = FinModeling::AnnualReportFiling.download ea_2011_annual_rpt
|
|
46
|
+
#filing.balance_sheet.is_valid?.should be_false
|
|
47
|
+
pending
|
|
48
|
+
end
|
|
49
|
+
it "returns false if the assets total does not match the liabilities and equity total" do
|
|
50
|
+
#ea_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/712515/000119312511149262/0001193125-11-149262-index.htm"
|
|
51
|
+
#filing = FinModeling::AnnualReportFiling.download ea_2011_annual_rpt
|
|
52
|
+
#filing.balance_sheet.is_valid?.should be_false
|
|
53
|
+
pending
|
|
54
|
+
end
|
|
55
|
+
it "returns true otherwise" do
|
|
56
|
+
@balance_sheet.is_valid?.should be_true
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe "reformulated" do
|
|
61
|
+
it "takes a period and returns a ReformulatedBalanceSheet" do
|
|
62
|
+
@balance_sheet.reformulated(@period).should be_an_instance_of FinModeling::ReformulatedBalanceSheet
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe "write_constructor" do
|
|
67
|
+
before(:all) do
|
|
68
|
+
file_name = "/tmp/finmodeling-bal-sheet.rb"
|
|
69
|
+
item_name = "@bal_sheet"
|
|
70
|
+
file = File.open(file_name, "w")
|
|
71
|
+
@balance_sheet.write_constructor(file, item_name)
|
|
72
|
+
file.close
|
|
73
|
+
|
|
74
|
+
eval(File.read(file_name))
|
|
75
|
+
|
|
76
|
+
@loaded_bs = eval(item_name)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "writes itself to a file, and when reloaded, has the same periods" do
|
|
80
|
+
expected_periods = @balance_sheet.periods.map{|x| x.to_pretty_s}.join(',')
|
|
81
|
+
@loaded_bs.periods.map{|x| x.to_pretty_s}.join(',').should == expected_periods
|
|
82
|
+
end
|
|
83
|
+
it "writes itself to a file, and when reloaded, has the same net operating assets" do
|
|
84
|
+
period = @balance_sheet.periods.last
|
|
85
|
+
expected_noa = @balance_sheet.reformulated(period).net_operating_assets.total
|
|
86
|
+
@loaded_bs.reformulated(period).net_operating_assets.total.should be_within(1.0).of(expected_noa)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
end
|
|
91
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# multi_column_calculation_summary_spec.rb
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe FinModeling::CalculationSummary do
|
|
6
|
+
before(:all) do
|
|
7
|
+
@summary = FinModeling::CalculationSummary.new
|
|
8
|
+
@summary.title = "CS 1"
|
|
9
|
+
@summary.rows = [ FinModeling::CalculationRow.new(:key => "Row", :vals => [nil, 0, nil, -101, 2.4]) ]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe "valid_vals" do
|
|
13
|
+
subject { @summary.rows.first.valid_vals }
|
|
14
|
+
it "should return all non-nil values" do
|
|
15
|
+
subject.should == @summary.rows[0].vals.select{ |x| !x.nil? }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe "filter_by_type" do
|
|
20
|
+
before(:all) do
|
|
21
|
+
@summary2 = FinModeling::CalculationSummary.new
|
|
22
|
+
@summary2.rows = [ ]
|
|
23
|
+
@summary2.rows << FinModeling::CalculationRow.new(:key => "Row", :type => :oa, :vals => [ 4])
|
|
24
|
+
@summary2.rows << FinModeling::CalculationRow.new(:key => "Row", :type => :fa, :vals => [109])
|
|
25
|
+
@summary2.rows << FinModeling::CalculationRow.new(:key => "Row", :type => :oa, :vals => [ 93])
|
|
26
|
+
@summary2.rows << FinModeling::CalculationRow.new(:key => "Row", :type => :fa, :vals => [ 1])
|
|
27
|
+
end
|
|
28
|
+
it "should return a new FinModeling::CalculationSummary" do
|
|
29
|
+
@summary2.filter_by_type(:oa).should be_an_instance_of FinModeling::CalculationSummary
|
|
30
|
+
end
|
|
31
|
+
it "should return a summary of only the requested type" do
|
|
32
|
+
@summary2.filter_by_type(:oa).rows.map{ |row| row.type }.uniq.should == [:oa]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe "+" do
|
|
37
|
+
before(:all) do
|
|
38
|
+
@mccs1 = FinModeling::CalculationSummary.new
|
|
39
|
+
@mccs1.title = "MCCS 1"
|
|
40
|
+
@mccs1.rows = [ FinModeling::CalculationRow.new(:key => "Row 1", :vals => [nil, 0, nil, -101, 2.4]) ]
|
|
41
|
+
|
|
42
|
+
@mccs2 = FinModeling::CalculationSummary.new
|
|
43
|
+
@mccs2.title = "MCCS 2"
|
|
44
|
+
@mccs2.rows = [ FinModeling::CalculationRow.new(:key => "Row 1", :vals => [nil, 0, nil, -101, 2.4]) ]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
subject { @mccs1 + @mccs2 }
|
|
48
|
+
|
|
49
|
+
it { should be_an_instance_of FinModeling::CalculationSummary }
|
|
50
|
+
|
|
51
|
+
its(:title) { should == @mccs1.title }
|
|
52
|
+
|
|
53
|
+
it "should set the row labels to the first summary's row labels" do
|
|
54
|
+
subject.rows.map{ |row| row.key }.should == @mccs1.rows.map{ |row| row.key }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "should merge the values of summary into an array of values in the result" do
|
|
58
|
+
0.upto(subject.rows.length-1).each do |row_idx|
|
|
59
|
+
subject.rows[row_idx].vals.should == ( @mccs1.rows[row_idx].vals + @mccs2.rows[row_idx].vals )
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
|
|
21
|
+
#puts "initial cash: #{@cash_initial}"
|
|
22
|
+
#puts "final cash: #{@cash_final}"
|
|
23
|
+
|
|
24
|
+
#@filing.cash_flow_statement.cash_change_calculation.summary(:period => @cfs_period_q1_thru_q3).print
|
|
25
|
+
|
|
26
|
+
#@cash_changes.leaf_items(:period => @cfs_period_q1_thru_q3).each do |item|
|
|
27
|
+
# puts "#{item.name}: #{item.def ? item.def["xbrli:balance"] : "nil"}"
|
|
28
|
+
#end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe "summary(period)" do
|
|
32
|
+
subject{ @cash_changes.summary(:period => @cfs_period_q1_thru_q3) }
|
|
33
|
+
it { should be_an_instance_of FinModeling::CalculationSummary }
|
|
34
|
+
|
|
35
|
+
it "should have values with the right sign" do
|
|
36
|
+
expected = [7033, 1011, 337, 1437, -61, 526, 3, -247, 268,
|
|
37
|
+
-146, 72, 255, 70, 83, -2487, -43693, 33107,
|
|
38
|
+
-358, 694, -395, -1350, -20, 61, 0, 8780, -8054, 74]
|
|
39
|
+
|
|
40
|
+
actual = subject.rows.map{|row| (row.vals.first/1000.0/1000.0).round}
|
|
41
|
+
|
|
42
|
+
if actual != expected
|
|
43
|
+
num_errors = actual.zip(expected).map{ |x,y| x==y ? 0 : 1 }.inject(:+)
|
|
44
|
+
puts "# errors: #{num_errors}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
actual.should == expected
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe ".total" do
|
|
51
|
+
subject{ @cash_changes.summary(:period => @cfs_period_q1_thru_q3).total }
|
|
52
|
+
it { should be_within(1.0).of(@cash_final - @cash_initial) }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# period_array_spec.rb
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe FinModeling::CashChangeItem do
|
|
6
|
+
|
|
7
|
+
before(:all) do
|
|
8
|
+
#FinModeling::CashChangeItem.load_vectors_and_train(FinModeling::CashChangeItem::TRAINING_VECTORS)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
describe "new" do
|
|
12
|
+
it "takes a string and returns a new CashChangeItem" do
|
|
13
|
+
cci = FinModeling::CashChangeItem.new("Depreciation and amortization of property and equipment")
|
|
14
|
+
cci.should be_an_instance_of FinModeling::CashChangeItem
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe "train" do
|
|
19
|
+
it "trains the classifier that this CashChangeItem is of the given type" do
|
|
20
|
+
FinModeling::CashChangeItem.new("Depreciation and amortization of property and equipment").train(:c)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "classification_estimates" do
|
|
25
|
+
it "returns a hash with the confidence in each CashChangeItem type" do
|
|
26
|
+
cci = FinModeling::CashChangeItem.new("Depreciation and amortization of property and equipment")
|
|
27
|
+
|
|
28
|
+
FinModeling::CashChangeItem::TYPES.each do |klass|
|
|
29
|
+
cci.classification_estimates.keys.include?(klass).should be_true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe "classify" do
|
|
35
|
+
it "returns the CashChangeItem type with the highest probability estimate" do
|
|
36
|
+
cci = FinModeling::CashChangeItem.new("Depreciation and amortization of property and equipment")
|
|
37
|
+
estimates = cci.classification_estimates
|
|
38
|
+
estimates[cci.classify].should be_within(0.1).of(estimates.values.max)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe "load_vectors_and_train" do
|
|
43
|
+
# the before(:all) clause calls load_vectors_and_train already
|
|
44
|
+
# we can just focus, here, on its effects
|
|
45
|
+
|
|
46
|
+
it "classifies >92% correctly" do # FIXME: add more vectors to tighten this up
|
|
47
|
+
num_items = 0
|
|
48
|
+
errors = []
|
|
49
|
+
FinModeling::CashChangeItem::TRAINING_VECTORS.each do |vector|
|
|
50
|
+
num_items += 1
|
|
51
|
+
cci = FinModeling::CashChangeItem.new(vector[:item_string])
|
|
52
|
+
if cci.classify != vector[:klass]
|
|
53
|
+
errors.push({ :cci=>cci.to_s, :expected=>vector[:klass], :got=>cci.classify })
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
pct_errors = errors.length.to_f / num_items
|
|
58
|
+
if pct_errors > 0.08
|
|
59
|
+
puts "errors: " + errors.inspect
|
|
60
|
+
end
|
|
61
|
+
pct_errors.should be < 0.08
|
|
62
|
+
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
it "returns the root node of the cash change calculation" do
|
|
17
|
+
@cash_flow_stmt.cash_change_calculation.label.downcase.should match /^cash/
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe "is_valid?" do
|
|
22
|
+
it "returns true if free cash flow matches financing flows and none are zero" do
|
|
23
|
+
re_cfs = @cash_flow_stmt.reformulated(@period)
|
|
24
|
+
flows_are_balanced = (re_cfs.free_cash_flow.total == (-1*re_cfs.financing_flows.total))
|
|
25
|
+
none_are_zero = (re_cfs.cash_from_operations.total != 0) &&
|
|
26
|
+
(re_cfs.cash_investments_in_operations.total != 0) &&
|
|
27
|
+
(re_cfs.payments_to_debtholders.total != 0) &&
|
|
28
|
+
(re_cfs.payments_to_stockholders.total != 0)
|
|
29
|
+
@cash_flow_stmt.is_valid?.should == (flows_are_balanced && none_are_zero)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe "reformulated" do
|
|
34
|
+
subject { @cash_flow_stmt.reformulated(@period) }
|
|
35
|
+
it { should be_an_instance_of FinModeling::ReformulatedCashFlowStatement }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe "latest_quarterly_reformulated" do
|
|
39
|
+
before(:all) do
|
|
40
|
+
google_2011_q1_rpt = "http://www.sec.gov/Archives/edgar/data/1288776/000119312511134428/0001193125-11-134428-index.htm"
|
|
41
|
+
@cash_flow_stmt_2011_q1 = FinModeling::AnnualReportFiling.download(google_2011_q1_rpt).cash_flow_statement
|
|
42
|
+
|
|
43
|
+
google_2011_q2_rpt = "http://www.sec.gov/Archives/edgar/data/1288776/000119312511199078/0001193125-11-199078-index.htm"
|
|
44
|
+
@cash_flow_stmt_2011_q2 = FinModeling::AnnualReportFiling.download(google_2011_q2_rpt).cash_flow_statement
|
|
45
|
+
|
|
46
|
+
google_2011_q3_rpt = "http://www.sec.gov/Archives/edgar/data/1288776/000119312511282235/0001193125-11-282235-index.htm"
|
|
47
|
+
@cash_flow_stmt_2011_q3 = FinModeling::AnnualReportFiling.download(google_2011_q3_rpt).cash_flow_statement
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
context "when given a Q1 report" do
|
|
51
|
+
subject { @cash_flow_stmt_2011_q1.latest_quarterly_reformulated(nil) }
|
|
52
|
+
it { should be_an_instance_of FinModeling::ReformulatedCashFlowStatement }
|
|
53
|
+
it "should be valid" do
|
|
54
|
+
subject.cash_investments_in_operations.total.abs.should be > 1.0
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
context "when given a Q2 report (and a previous Q1 report)" do
|
|
59
|
+
subject { @cash_flow_stmt_2011_q2.latest_quarterly_reformulated(@cash_flow_stmt_2011_q1) }
|
|
60
|
+
it { should be_an_instance_of FinModeling::ReformulatedCashFlowStatement }
|
|
61
|
+
it "should be valid" do
|
|
62
|
+
subject.cash_investments_in_operations.total.abs.should be > 1.0
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
context "when given a Q3 report (and a previous Q2 report)" do
|
|
67
|
+
subject { @cash_flow_stmt_2011_q3.latest_quarterly_reformulated(@cash_flow_stmt_2011_q2) }
|
|
68
|
+
it { should be_an_instance_of FinModeling::ReformulatedCashFlowStatement }
|
|
69
|
+
it "should be valid" do
|
|
70
|
+
subject.cash_investments_in_operations.total.abs.should be > 1.0
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
context "when given an annual report (and a previous Q3 report)" do
|
|
75
|
+
subject { @cash_flow_stmt.latest_quarterly_reformulated(@cash_flow_stmt_2011_q3) }
|
|
76
|
+
it { should be_an_instance_of FinModeling::ReformulatedCashFlowStatement }
|
|
77
|
+
it "should be valid" do
|
|
78
|
+
subject.cash_investments_in_operations.total.abs.should be > 1.0
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe "write_constructor" do
|
|
84
|
+
before(:all) do
|
|
85
|
+
file_name = "/tmp/finmodeling-cash_flow_stmt.rb"
|
|
86
|
+
item_name = "@cfs"
|
|
87
|
+
file = File.open(file_name, "w")
|
|
88
|
+
@cash_flow_stmt.write_constructor(file, item_name)
|
|
89
|
+
file.close
|
|
90
|
+
|
|
91
|
+
eval(File.read(file_name))
|
|
92
|
+
|
|
93
|
+
@loaded_cfs = eval(item_name)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "writes itself to a file, and when reloaded, has the same periods" do
|
|
97
|
+
expected_periods = @cash_flow_stmt.periods.map{|x| x.to_pretty_s}.join(',')
|
|
98
|
+
@loaded_cfs.periods.map{|x| x.to_pretty_s}.join(',').should == expected_periods
|
|
99
|
+
end
|
|
100
|
+
it "writes itself to a file, and when reloaded, has the same change in cash" do
|
|
101
|
+
period = @cash_flow_stmt.periods.last
|
|
102
|
+
expected_cash_change = @cash_flow_stmt.cash_change_calculation.summary(:period=>period).total
|
|
103
|
+
@loaded_cfs.cash_change_calculation.summary(:period=>period).total.should be_within(1.0).of(expected_cash_change)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
end
|
|
108
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
describe "new" do
|
|
12
|
+
before(:each) do
|
|
13
|
+
@calculation = FinModeling::Mocks::Calculation.new
|
|
14
|
+
end
|
|
15
|
+
it "takes a taxonomy and a xbrlware calculation and returns a CompanyFilingCalculation" do
|
|
16
|
+
FinModeling::CompanyFilingCalculation.new(@calculation).should be_an_instance_of FinModeling::CompanyFilingCalculation
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "label" do
|
|
21
|
+
before(:each) do
|
|
22
|
+
@calculation = FinModeling::Mocks::Calculation.new
|
|
23
|
+
end
|
|
24
|
+
it "returns the calculation's label" do
|
|
25
|
+
cfc = FinModeling::CompanyFilingCalculation.new(@calculation)
|
|
26
|
+
cfc.label.should == @calculation.label
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe "periods" do
|
|
31
|
+
it "returns a PeriodArray, which helps filter and choose periods" do
|
|
32
|
+
@filing.balance_sheet.periods.should be_an_instance_of FinModeling::PeriodArray
|
|
33
|
+
end
|
|
34
|
+
it "returns an array of the periods over/at which this calculation can be queried" do
|
|
35
|
+
@filing.balance_sheet.periods.map{|x| x.to_s }.sort.should == ["2008-12-31", "2009-12-31", "2010-12-31", "2011-12-31"]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe "leaf_items" do
|
|
40
|
+
before(:all) do
|
|
41
|
+
@assets = @filing.balance_sheet.assets_calculation
|
|
42
|
+
@period = @filing.balance_sheet.periods.last
|
|
43
|
+
end
|
|
44
|
+
it "returns an array of the leaf items in the calculation tree that match the period" do
|
|
45
|
+
@assets.leaf_items(:period => @period).length.should == 12
|
|
46
|
+
end
|
|
47
|
+
it "returns an array of the leaf items in the calculation tree that match the period" do
|
|
48
|
+
@assets.leaf_items(:period => @period).first.should be_an_instance_of Xbrlware::Item
|
|
49
|
+
end
|
|
50
|
+
it "returns all leaf items, if no period given" do
|
|
51
|
+
@assets.leaf_items.length.should == 26
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe "leaf_items_sum" do
|
|
56
|
+
before(:all) do
|
|
57
|
+
vepc_2010_annual_rpt = "http://www.sec.gov/Archives/edgar/data/103682/000119312511049905/d10k.htm"
|
|
58
|
+
@filing_with_mixed_order = FinModeling::AnnualReportFiling.download vepc_2010_annual_rpt
|
|
59
|
+
|
|
60
|
+
# this balance sheet has some items (accumulated depreciation) that
|
|
61
|
+
# should be subtracted from total assets, which makes it a better test
|
|
62
|
+
# than the google annual report
|
|
63
|
+
balance_sheet = @filing_with_mixed_order.balance_sheet
|
|
64
|
+
@assets = balance_sheet.assets_calculation
|
|
65
|
+
@period = balance_sheet.periods.last
|
|
66
|
+
end
|
|
67
|
+
it "returns the sum of the calculation tree, in the given period" do
|
|
68
|
+
mapping = Xbrlware::ValueMapping.new
|
|
69
|
+
mapping.policy[:credit] = :flip
|
|
70
|
+
|
|
71
|
+
@assets.leaf_items_sum(:period=>@period, :mapping=>mapping).should be_within(1.0).of(42817000000.0)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|