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.
Files changed (97) hide show
  1. data/.gitignore +3 -0
  2. data/Gemfile +10 -0
  3. data/README.md +292 -0
  4. data/Rakefile +6 -0
  5. data/TODO.txt +36 -0
  6. data/examples/dump_report.rb +33 -0
  7. data/examples/lists/nasdaq-mid-to-mega-tech-symbols.txt +226 -0
  8. data/examples/show_report.rb +218 -0
  9. data/examples/show_reports.rb +77 -0
  10. data/finmodeling.gemspec +31 -0
  11. data/lib/finmodeling/annual_report_filing.rb +104 -0
  12. data/lib/finmodeling/array_with_stats.rb +22 -0
  13. data/lib/finmodeling/assets_calculation.rb +36 -0
  14. data/lib/finmodeling/assets_item.rb +14 -0
  15. data/lib/finmodeling/assets_item_vectors.rb +638 -0
  16. data/lib/finmodeling/balance_sheet_analyses.rb +33 -0
  17. data/lib/finmodeling/balance_sheet_calculation.rb +68 -0
  18. data/lib/finmodeling/calculation_summary.rb +148 -0
  19. data/lib/finmodeling/can_cache_classifications.rb +36 -0
  20. data/lib/finmodeling/can_cache_summaries.rb +16 -0
  21. data/lib/finmodeling/can_classify_rows.rb +54 -0
  22. data/lib/finmodeling/cash_change_calculation.rb +67 -0
  23. data/lib/finmodeling/cash_change_item.rb +14 -0
  24. data/lib/finmodeling/cash_change_item_vectors.rb +241 -0
  25. data/lib/finmodeling/cash_flow_statement_calculation.rb +85 -0
  26. data/lib/finmodeling/classifiers.rb +11 -0
  27. data/lib/finmodeling/company.rb +102 -0
  28. data/lib/finmodeling/company_filing.rb +64 -0
  29. data/lib/finmodeling/company_filing_calculation.rb +75 -0
  30. data/lib/finmodeling/company_filings.rb +100 -0
  31. data/lib/finmodeling/config.rb +37 -0
  32. data/lib/finmodeling/constant_forecasting_policy.rb +23 -0
  33. data/lib/finmodeling/factory.rb +27 -0
  34. data/lib/finmodeling/float_helpers.rb +17 -0
  35. data/lib/finmodeling/forecasts.rb +48 -0
  36. data/lib/finmodeling/generic_forecasting_policy.rb +19 -0
  37. data/lib/finmodeling/has_string_classifer.rb +96 -0
  38. data/lib/finmodeling/income_statement_analyses.rb +74 -0
  39. data/lib/finmodeling/income_statement_calculation.rb +71 -0
  40. data/lib/finmodeling/income_statement_item.rb +14 -0
  41. data/lib/finmodeling/income_statement_item_vectors.rb +654 -0
  42. data/lib/finmodeling/liabs_and_equity_calculation.rb +36 -0
  43. data/lib/finmodeling/liabs_and_equity_item.rb +14 -0
  44. data/lib/finmodeling/liabs_and_equity_item_vectors.rb +1936 -0
  45. data/lib/finmodeling/net_income_calculation.rb +41 -0
  46. data/lib/finmodeling/paths.rb +5 -0
  47. data/lib/finmodeling/period_array.rb +24 -0
  48. data/lib/finmodeling/quarterly_report_filing.rb +23 -0
  49. data/lib/finmodeling/rate.rb +20 -0
  50. data/lib/finmodeling/ratio.rb +20 -0
  51. data/lib/finmodeling/reformulated_balance_sheet.rb +176 -0
  52. data/lib/finmodeling/reformulated_cash_flow_statement.rb +140 -0
  53. data/lib/finmodeling/reformulated_income_statement.rb +436 -0
  54. data/lib/finmodeling/string_helpers.rb +26 -0
  55. data/lib/finmodeling/version.rb +3 -0
  56. data/lib/finmodeling.rb +70 -0
  57. data/spec/annual_report_filing_spec.rb +68 -0
  58. data/spec/assets_calculation_spec.rb +21 -0
  59. data/spec/assets_item_spec.rb +66 -0
  60. data/spec/balance_sheet_analyses_spec.rb +43 -0
  61. data/spec/balance_sheet_calculation_spec.rb +91 -0
  62. data/spec/calculation_summary_spec.rb +63 -0
  63. data/spec/can_classify_rows_spec.rb +86 -0
  64. data/spec/cash_change_calculation_spec.rb +56 -0
  65. data/spec/cash_change_item_spec.rb +66 -0
  66. data/spec/cash_flow_statement_calculation_spec.rb +108 -0
  67. data/spec/company_filing_calculation_spec.rb +74 -0
  68. data/spec/company_filing_spec.rb +30 -0
  69. data/spec/company_filings_spec.rb +55 -0
  70. data/spec/company_spec.rb +73 -0
  71. data/spec/constant_forecasting_policy_spec.rb +37 -0
  72. data/spec/factory_spec.rb +18 -0
  73. data/spec/forecasts_spec.rb +21 -0
  74. data/spec/generic_forecasting_policy_spec.rb +33 -0
  75. data/spec/income_statement_analyses_spec.rb +63 -0
  76. data/spec/income_statement_calculation_spec.rb +88 -0
  77. data/spec/income_statement_item_spec.rb +86 -0
  78. data/spec/liabs_and_equity_calculation_spec.rb +20 -0
  79. data/spec/liabs_and_equity_item_spec.rb +66 -0
  80. data/spec/mocks/calculation.rb +10 -0
  81. data/spec/mocks/income_statement_analyses.rb +93 -0
  82. data/spec/mocks/sec_query.rb +31 -0
  83. data/spec/net_income_calculation_spec.rb +23 -0
  84. data/spec/period_array.rb +52 -0
  85. data/spec/quarterly_report_filing_spec.rb +69 -0
  86. data/spec/rate_spec.rb +33 -0
  87. data/spec/ratio_spec.rb +33 -0
  88. data/spec/reformulated_balance_sheet_spec.rb +146 -0
  89. data/spec/reformulated_cash_flow_statement_spec.rb +174 -0
  90. data/spec/reformulated_income_statement_spec.rb +293 -0
  91. data/spec/spec_helper.rb +5 -0
  92. data/spec/string_helpers_spec.rb +23 -0
  93. data/tools/create_balance_sheet_training_vectors.rb +65 -0
  94. data/tools/create_cash_change_training_vectors.rb +48 -0
  95. data/tools/create_credit_debit_training_vectors.rb +51 -0
  96. data/tools/create_income_statement_training_vectors.rb +48 -0
  97. metadata +289 -0
@@ -0,0 +1,33 @@
1
+ module FinModeling
2
+
3
+ class BalanceSheetAnalyses < CalculationSummary
4
+ def initialize(calc_summary)
5
+ @title = calc_summary.title
6
+ @rows = calc_summary.rows
7
+ @header_row = calc_summary.header_row
8
+ @key_width = calc_summary.key_width
9
+ @val_width = calc_summary.val_width
10
+ @max_decimals = calc_summary.max_decimals
11
+ @totals_row_enabled = false
12
+ end
13
+
14
+ def print_extras # FIXME: rename
15
+ lr = noa_growth_row.valid_vals.linear_regression
16
+ puts "\t\tNOA growth: "+
17
+ "a:#{lr.a.to_s.cap_decimals(4)}, "+
18
+ "b:#{lr.b.to_s.cap_decimals(4)}, "+
19
+ "r:#{lr.r.to_s.cap_decimals(4)}, "+
20
+ "var:#{noa_growth_row.valid_vals.variance.to_s.cap_decimals(4)}"
21
+ end
22
+
23
+ def noa_growth_row
24
+ find_row_by_key('NOA Growth')
25
+ end
26
+
27
+ def find_row_by_key(key) # FIXME: move this to CalculationSummary
28
+ self.rows.find{ |x| x.key == key }
29
+ end
30
+ end
31
+
32
+ end
33
+
@@ -0,0 +1,68 @@
1
+ module FinModeling
2
+ class BalanceSheetCalculation < CompanyFilingCalculation
3
+
4
+ def assets_calculation
5
+ if @assets.nil?
6
+ friendly_goal = "assets"
7
+ label_regexes = [ /(^total *|^consolidated *|^)assets$/,
8
+ /^assets total$/ ]
9
+ id_regexes = [ /^(|Locator_|loc_)(|us-gaap_)Assets[_a-z0-9]+/ ]
10
+
11
+ calc = find_and_verify_calculation_arc(friendly_goal, label_regexes, id_regexes)
12
+ @assets = AssetsCalculation.new(calc)
13
+ end
14
+ return @assets
15
+ end
16
+
17
+ def liabs_and_equity_calculation
18
+ if @liabs_and_equity.nil?
19
+ friendly_goal = "liabilities and equity"
20
+ label_regexes = [ /(^total *|^)liabilities.*and.*equity/ ]
21
+ id_regexes = [ /.*/ ] # no checking...
22
+
23
+ calc = find_and_verify_calculation_arc(friendly_goal, label_regexes, id_regexes)
24
+ @liabs_and_equity = LiabsAndEquityCalculation.new(calc)
25
+ end
26
+ return @liabs_and_equity
27
+ end
28
+
29
+ def is_valid?
30
+ has_cash_item = false
31
+ assets_calculation.leaf_items.each do |leaf|
32
+ if !has_cash_item and leaf.name.downcase.matches_regexes?([/cash/])
33
+ has_cash_item = true
34
+ end
35
+ end
36
+
37
+ has_equity_item = false
38
+ liabs_and_equity_calculation.leaf_items.each do |leaf|
39
+ if !has_equity_item and leaf.name.downcase.matches_regexes?([/equity/, /stock/])
40
+ has_equity_item = true
41
+ end
42
+ end
43
+
44
+ left = assets_calculation.leaf_items_sum(:period => periods.last)
45
+ right = liabs_and_equity_calculation.leaf_items_sum(:period => periods.last)
46
+ allowed_error = 1.0
47
+ is_balanced = (left - right) < allowed_error
48
+
49
+ puts "balance sheet's assets calculation lacks cash item" if !has_cash_item
50
+ puts "balance sheet's liabilities and equity calculation lacks equity item" if !has_equity_item
51
+ puts "balance sheet's isn't balanced (#{left}, #{right})" if !is_balanced
52
+ return (has_cash_item and has_equity_item and is_balanced)
53
+ end
54
+
55
+ def reformulated(period)
56
+ return ReformulatedBalanceSheet.new(period,
57
+ assets_calculation.summary(:period=>period),
58
+ liabs_and_equity_calculation.summary(:period=>period))
59
+ end
60
+
61
+ def write_constructor(file, item_name)
62
+ item_calc_name = item_name + "_calc"
63
+ @calculation.write_constructor(file, item_calc_name)
64
+ file.puts "#{item_name} = FinModeling::BalanceSheetCalculation.new(#{item_calc_name})"
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,148 @@
1
+ module FinModeling
2
+
3
+ class CalculationRow
4
+ attr_accessor :key, :type, :vals
5
+
6
+ def initialize(args = {})
7
+ @key = args[:key] || ""
8
+ @type = args[:type]
9
+ @vals = args[:vals] || []
10
+ end
11
+
12
+ def valid_vals
13
+ ArrayWithStats.new(@vals.select{ |val| !val.nil? })
14
+ end
15
+
16
+ def print(key_width=18, max_decimals=4, val_width=12)
17
+ justified_key = @key.fixed_width_left_justify(key_width)
18
+
19
+ justified_vals = ""
20
+ @vals.each do |val|
21
+ val_with_commas = val.to_s.with_thousands_separators
22
+ justified_vals += " " + val_with_commas.cap_decimals(max_decimals).fixed_width_right_justify(val_width)
23
+ end
24
+
25
+ puts "\t" + justified_key + justified_vals
26
+ end
27
+
28
+ def write_constructor(file, item_name)
29
+ file.puts "args = { }"
30
+ file.puts "args[:key] = \"#{@key}\""
31
+ file.puts "args[:type] = \"#{@type}\""
32
+ file.puts "args[:vals] = [#{@vals.join(', ')}]"
33
+ file.puts "#{item_name} = FinModeling::CalculationRow.new(args)"
34
+ end
35
+ end
36
+
37
+ class CalculationHeader < CalculationRow
38
+ def print(key_width=18, max_decimals=4, val_width=12)
39
+ justified_key = @key.fixed_width_left_justify(key_width)
40
+
41
+ justified_vals = ""
42
+ @vals.each do |val|
43
+ justified_vals += " " + val.fixed_width_right_justify(val_width)
44
+ end
45
+
46
+ puts "\t" + justified_key + justified_vals
47
+ end
48
+
49
+ def write_constructor(file, item_name)
50
+ file.puts "args = { }"
51
+ file.puts "args[:key] = \"#{@key}\""
52
+ file.puts "args[:vals] = [#{@vals.map{ |val| "\"#{val}\"" }.join(', ')}]"
53
+ file.puts "#{item_name} = FinModeling::CalculationHeader.new(args)"
54
+ end
55
+ end
56
+
57
+ class CalculationSummary
58
+ attr_accessor :title, :header_row, :rows
59
+ attr_accessor :key_width, :val_width, :max_decimals, :totals_row_enabled
60
+
61
+ def initialize
62
+ @key_width = 18
63
+ @val_width = 12
64
+ @max_decimals = 4
65
+ @totals_row_enabled = true
66
+ end
67
+
68
+ def num_value_columns
69
+ @rows.map{ |row| row.vals.length }.max
70
+ end
71
+
72
+ def total(col_idx=0)
73
+ @rows.map{ |row| row.vals[col_idx] }.inject(:+) || 0.0
74
+ end
75
+
76
+ def totals
77
+ return [] if num_value_columns.nil?
78
+ 0.upto(num_value_columns-1).map { |col_idx| total(col_idx) }
79
+ end
80
+
81
+ def print
82
+ puts @title
83
+
84
+ rows = []
85
+ rows << @header_row if @header_row
86
+ rows += @rows
87
+ rows << CalculationRow.new(:key => "Total", :vals => totals) if @totals_row_enabled
88
+ rows.each { |row| row.print(@key_width, @max_decimals, @val_width) }
89
+
90
+ puts
91
+ end
92
+
93
+ def filter_by_type(type)
94
+ cs = CalculationSummary.new
95
+ cs.title = @title
96
+ cs.rows = @rows.select{ |x| x.type == type }
97
+ return cs
98
+ end
99
+
100
+ def write_constructor(file, item_name)
101
+ file.puts "#{item_name} = FinModeling::CalculationSummary.new"
102
+ file.puts "#{item_name}.title = \"#{@title}\""
103
+ file.puts "#{item_name}.key_width = #{@key_width}"
104
+ file.puts "#{item_name}.val_width = #{@val_width}"
105
+ file.puts "#{item_name}.max_decimals = #{@max_decimals}"
106
+ file.puts "#{item_name}.totals_row_enabled = #{@totals_row_enabled}"
107
+
108
+ if @header_row
109
+ header_row_item_name = item_name + "_header_row"
110
+ @header_row.write_constructor(file, header_row_item_name)
111
+ file.puts "#{item_name}.header_row = #{header_row_item_name}"
112
+ end
113
+
114
+ row_item_names = []
115
+ @rows.each_with_index do |row, index|
116
+ row_item_name = item_name + "_row#{index}"
117
+ row.write_constructor(file, row_item_name)
118
+ row_item_names << row_item_name
119
+ end
120
+ file.puts "#{item_name}.rows = [#{row_item_names.join(',')}]"
121
+ end
122
+
123
+ def +(mccs)
124
+ raise RuntimeError.new("can't add a CalculationSummary to a #{mccs.class}") if !mccs.is_a?(CalculationSummary)
125
+ raise RuntimeError.new("can't add CalculationSummaries with different numbers of rows") if @rows.length != mccs.rows.length
126
+
127
+ multics = CalculationSummary.new
128
+ multics.title = @title
129
+
130
+ if @header_row
131
+ multics.header_row = CalculationHeader.new(
132
+ :key => @header_row.key.dup,
133
+ :vals => @header_row.vals.dup + mccs.header_row.vals.dup)
134
+ end
135
+
136
+ multics.rows = []
137
+ 0.upto(@rows.length-1).each do |idx|
138
+ multics.rows << CalculationRow.new(
139
+ :key => @rows[idx].key.dup,
140
+ :vals => @rows[idx].vals.dup + mccs.rows[idx].vals.dup)
141
+ end
142
+
143
+ return multics
144
+ end
145
+
146
+ end
147
+
148
+ end
@@ -0,0 +1,36 @@
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
@@ -0,0 +1,16 @@
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
@@ -0,0 +1,54 @@
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
@@ -0,0 +1,67 @@
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? and 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) or 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_regexes?([ /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
@@ -0,0 +1,14 @@
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