finmodeling 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. data/.gitignore +0 -0
  2. data/Gemfile +2 -0
  3. data/README.md +289 -269
  4. data/Rakefile +12 -0
  5. data/TODO.txt +113 -20
  6. data/examples/{dump_report.rb → dump_latest_10k.rb} +1 -1
  7. data/examples/list_disclosures.rb +50 -0
  8. data/examples/lists/nasdaq-mid-to-mega-tech-symbols.txt +0 -0
  9. data/examples/show_report.rb +112 -32
  10. data/examples/show_reports.rb +162 -33
  11. data/finmodeling.gemspec +4 -1
  12. data/lib/finmodeling/annual_report_filing.rb +97 -18
  13. data/lib/finmodeling/array_with_stats.rb +0 -0
  14. data/lib/finmodeling/assets_calculation.rb +12 -3
  15. data/lib/finmodeling/assets_item.rb +0 -0
  16. data/lib/finmodeling/assets_item_vectors.rb +0 -0
  17. data/lib/finmodeling/balance_sheet_analyses.rb +19 -4
  18. data/lib/finmodeling/balance_sheet_calculation.rb +52 -37
  19. data/lib/finmodeling/calculation_summary.rb +119 -14
  20. data/lib/finmodeling/can_cache_classifications.rb +0 -0
  21. data/lib/finmodeling/can_cache_summaries.rb +0 -0
  22. data/lib/finmodeling/can_choose_successive_periods.rb +15 -0
  23. data/lib/finmodeling/can_classify_rows.rb +0 -0
  24. data/lib/finmodeling/capm.rb +80 -0
  25. data/lib/finmodeling/cash_change_calculation.rb +3 -3
  26. data/lib/finmodeling/cash_change_item.rb +0 -0
  27. data/lib/finmodeling/cash_change_item_vectors.rb +0 -0
  28. data/lib/finmodeling/cash_change_summary_from_differences.rb +36 -0
  29. data/lib/finmodeling/cash_flow_statement_analyses.rb +36 -0
  30. data/lib/finmodeling/cash_flow_statement_calculation.rb +28 -52
  31. data/lib/finmodeling/classifiers.rb +2 -0
  32. data/lib/finmodeling/company.rb +0 -0
  33. data/lib/finmodeling/company_filing.rb +30 -7
  34. data/lib/finmodeling/company_filing_calculation.rb +16 -6
  35. data/lib/finmodeling/company_filings.rb +112 -46
  36. data/lib/finmodeling/comprehensive_income_calculation.rb +60 -0
  37. data/lib/finmodeling/comprehensive_income_statement_calculation.rb +74 -0
  38. data/lib/finmodeling/comprehensive_income_statement_item.rb +20 -0
  39. data/lib/finmodeling/comprehensive_income_statement_item_vectors.rb +235 -0
  40. data/lib/finmodeling/config.rb +0 -0
  41. data/lib/finmodeling/debt_cost_of_capital.rb +14 -0
  42. data/lib/finmodeling/equity_change_calculation.rb +43 -0
  43. data/lib/finmodeling/equity_change_item.rb +25 -0
  44. data/lib/finmodeling/equity_change_item_vectors.rb +156 -0
  45. data/lib/finmodeling/factory.rb +0 -0
  46. data/lib/finmodeling/fama_french_cost_of_equity.rb +119 -0
  47. data/lib/finmodeling/float_helpers.rb +14 -8
  48. data/lib/finmodeling/forecasted_reformulated_balance_sheet.rb +55 -0
  49. data/lib/finmodeling/forecasted_reformulated_income_statement.rb +110 -0
  50. data/lib/finmodeling/forecasts.rb +4 -4
  51. data/lib/finmodeling/has_string_classifer.rb +0 -0
  52. data/lib/finmodeling/income_statement_analyses.rb +23 -17
  53. data/lib/finmodeling/income_statement_calculation.rb +46 -43
  54. data/lib/finmodeling/income_statement_item.rb +1 -1
  55. data/lib/finmodeling/income_statement_item_vectors.rb +24 -13
  56. data/lib/finmodeling/invalid_filing_error.rb +4 -0
  57. data/lib/finmodeling/liabs_and_equity_calculation.rb +18 -8
  58. data/lib/finmodeling/liabs_and_equity_item.rb +1 -1
  59. data/lib/finmodeling/liabs_and_equity_item_vectors.rb +24 -24
  60. data/lib/finmodeling/linear_trend_forecasting_policy.rb +23 -0
  61. data/lib/finmodeling/net_income_calculation.rb +23 -10
  62. data/lib/finmodeling/net_income_summary_from_differences.rb +51 -0
  63. data/lib/finmodeling/paths.rb +0 -0
  64. data/lib/finmodeling/period_array.rb +8 -4
  65. data/lib/finmodeling/quarterly_report_filing.rb +9 -4
  66. data/lib/finmodeling/rate.rb +8 -0
  67. data/lib/finmodeling/ratio.rb +0 -0
  68. data/lib/finmodeling/reformulated_balance_sheet.rb +47 -88
  69. data/lib/finmodeling/reformulated_cash_flow_statement.rb +18 -41
  70. data/lib/finmodeling/reformulated_income_statement.rb +44 -206
  71. data/lib/finmodeling/reformulated_shareholder_equity_statement.rb +50 -0
  72. data/lib/finmodeling/reoi_valuation.rb +104 -0
  73. data/lib/finmodeling/shareholder_equity_statement_calculation.rb +34 -0
  74. data/lib/finmodeling/string_helpers.rb +18 -1
  75. data/lib/finmodeling/time_series_estimator.rb +25 -0
  76. data/lib/finmodeling/trailing_avg_forecasting_policy.rb +23 -0
  77. data/lib/finmodeling/version.rb +1 -1
  78. data/lib/finmodeling/weighted_avg_cost_of_capital.rb +35 -0
  79. data/lib/finmodeling/yahoo_finance_helpers.rb +20 -0
  80. data/lib/finmodeling.rb +33 -2
  81. data/spec/annual_report_filing_spec.rb +81 -45
  82. data/spec/assets_calculation_spec.rb +7 -4
  83. data/spec/assets_item_spec.rb +9 -14
  84. data/spec/balance_sheet_analyses_spec.rb +13 -13
  85. data/spec/balance_sheet_calculation_spec.rb +45 -51
  86. data/spec/calculation_summary_spec.rb +113 -21
  87. data/spec/can_classify_rows_spec.rb +0 -0
  88. data/spec/cash_change_calculation_spec.rb +1 -10
  89. data/spec/cash_change_item_spec.rb +10 -18
  90. data/spec/cash_flow_statement_calculation_spec.rb +10 -24
  91. data/spec/company_beta_spec.rb +53 -0
  92. data/spec/company_filing_calculation_spec.rb +39 -49
  93. data/spec/company_filing_spec.rb +0 -0
  94. data/spec/company_filings_spec.rb +75 -25
  95. data/spec/company_spec.rb +37 -47
  96. data/spec/comprehensive_income_statement_calculation_spec.rb +54 -0
  97. data/spec/comprehensive_income_statement_item_spec.rb +56 -0
  98. data/spec/debt_cost_of_capital_spec.rb +19 -0
  99. data/spec/equity_change_calculation_spec.rb +33 -0
  100. data/spec/equity_change_item_spec.rb +58 -0
  101. data/spec/factory_spec.rb +2 -2
  102. data/spec/forecasts_spec.rb +2 -2
  103. data/spec/income_statement_analyses_spec.rb +23 -21
  104. data/spec/income_statement_calculation_spec.rb +17 -49
  105. data/spec/income_statement_item_spec.rb +17 -29
  106. data/spec/liabs_and_equity_calculation_spec.rb +6 -3
  107. data/spec/liabs_and_equity_item_spec.rb +14 -22
  108. data/spec/linear_trend_forecasting_policy_spec.rb +37 -0
  109. data/spec/matchers/custom_matchers.rb +79 -0
  110. data/spec/mocks/calculation.rb +0 -0
  111. data/spec/mocks/income_statement_analyses.rb +0 -0
  112. data/spec/mocks/sec_query.rb +0 -0
  113. data/spec/net_income_calculation_spec.rb +16 -10
  114. data/spec/period_array.rb +0 -0
  115. data/spec/quarterly_report_filing_spec.rb +21 -38
  116. data/spec/rate_spec.rb +0 -0
  117. data/spec/ratio_spec.rb +0 -0
  118. data/spec/reformulated_balance_sheet_spec.rb +56 -33
  119. data/spec/reformulated_cash_flow_statement_spec.rb +18 -10
  120. data/spec/reformulated_income_statement_spec.rb +16 -15
  121. data/spec/reformulated_shareholder_equity_statement_spec.rb +43 -0
  122. data/spec/reoi_valuation_spec.rb +146 -0
  123. data/spec/shareholder_equity_statement_calculation_spec.rb +59 -0
  124. data/spec/spec_helper.rb +4 -1
  125. data/spec/string_helpers_spec.rb +15 -13
  126. data/spec/time_series_estimator_spec.rb +61 -0
  127. data/spec/trailing_avg_forecasting_policy_spec.rb +37 -0
  128. data/spec/weighted_avg_cost_of_capital_spec.rb +32 -0
  129. data/tools/create_equity_change_training_vectors.rb +49 -0
  130. data/tools/time_specs.sh +7 -0
  131. metadata +182 -36
  132. data/lib/finmodeling/constant_forecasting_policy.rb +0 -23
  133. data/lib/finmodeling/generic_forecasting_policy.rb +0 -19
  134. data/spec/constant_forecasting_policy_spec.rb +0 -37
  135. data/spec/generic_forecasting_policy_spec.rb +0 -33
@@ -4,12 +4,14 @@ module FinModeling
4
4
 
5
5
  CONSTRUCTOR_PATH = File.join(FinModeling::BASE_PATH, "constructors/")
6
6
  SCHEMA_VERSION_ITEM = "@schema_version"
7
- CURRENT_SCHEMA_VERSION = 1.1
7
+ CURRENT_SCHEMA_VERSION = 1.3
8
8
  # History:
9
9
  # 1.0: initial version
10
10
  # 1.1: added CFS to quarterly filings
11
11
  # added disclosures
12
12
  # renamed fake(.*)report to cached(.*)report
13
+ # 1.2: added shareholders' equity statement
14
+ # 1.3: added comprehensive income statement
13
15
 
14
16
  def self.download(url)
15
17
  uid = url.split("/")[-2..-1].join('-').gsub(/\.[A-zA-z]*$/, '')
@@ -37,11 +39,13 @@ module FinModeling
37
39
  def balance_sheet
38
40
  if @balance_sheet.nil?
39
41
  calculations=@taxonomy.callb.calculation
40
- bal_sheet = calculations.find{ |x| (x.clean_downcased_title =~ /statement.*financial.*position/) or
41
- (x.clean_downcased_title =~ /statement.*financial.*condition/) or
42
- (x.clean_downcased_title =~ /balance.*sheet/) }
42
+ bal_sheet = calculations.find{ |x| ((x.clean_downcased_title =~ /statement.*financial.*position/) ||
43
+ (x.clean_downcased_title =~ /statement.*financial.*condition/) ||
44
+ (x.clean_downcased_title =~ /balance.*sheet/)) &&
45
+ !(x.clean_downcased_title =~ /^balances included/) &&
46
+ !(x.clean_downcased_title =~ /net of tax/) }
43
47
  if bal_sheet.nil?
44
- raise RuntimeError.new("Couldn't find balance sheet in: " + calculations.map{ |x| "\"#{x.clean_downcased_title}\"" }.join("; "))
48
+ raise InvalidFilingError.new("Couldn't find balance sheet in: " + calculations.map{ |x| "\"#{x.clean_downcased_title}\"" }.join("; "))
45
49
  end
46
50
 
47
51
  @balance_sheet = BalanceSheetCalculation.new(bal_sheet)
@@ -52,12 +56,15 @@ module FinModeling
52
56
  def income_statement
53
57
  if @income_stmt.nil?
54
58
  calculations=@taxonomy.callb.calculation
55
- inc_stmt = calculations.find{ |x| (x.clean_downcased_title =~ /statement.*operations/) or
56
- (x.clean_downcased_title =~ /statement[s]*.*of.*earnings/) or
57
- (x.clean_downcased_title =~ /statement[s]*.*of.*income/) or
58
- (x.clean_downcased_title =~ /statement[s]*.*of.*net.*income/) }
59
+ inc_stmt = calculations.find{ |x| ((x.clean_downcased_title =~ /statement(|s).*operations/) ||
60
+ (x.clean_downcased_title =~ /statement(|s).*of.*earnings/) ||
61
+ (x.clean_downcased_title =~ /statement(|s).*of.*(|net.*)income/) ||
62
+ (x.clean_downcased_title =~ /(|net.*)income.*statement(|s)/)) &&
63
+ !(x.clean_downcased_title =~ /comprehensive/) &&
64
+ !(x.clean_downcased_title =~ /schedule/) &&
65
+ !(x.clean_downcased_title =~ /disclosure/) }
59
66
  if inc_stmt.nil?
60
- raise RuntimeError.new("Couldn't find income statement in: " + calculations.map{ |x| "\"#{x.clean_downcased_title}\"" }.join("; "))
67
+ raise InvalidFilingError.new("Couldn't find income statement in: " + calculations.map{ |x| "\"#{x.clean_downcased_title}\"" }.join("; "))
61
68
  end
62
69
 
63
70
  @income_stmt = IncomeStatementCalculation.new(inc_stmt)
@@ -65,13 +72,48 @@ module FinModeling
65
72
  return @income_stmt
66
73
  end
67
74
 
75
+ def has_an_income_statement?
76
+ begin
77
+ return income_statement ? true : false
78
+ rescue
79
+ return false
80
+ end
81
+ end
82
+
83
+ def comprehensive_income_statement
84
+ if @comprehensive_income_stmt.nil?
85
+ calculations=@taxonomy.callb.calculation
86
+ inc_stmt = calculations.find{ |x| ((x.clean_downcased_title =~ /statement.*operations/) ||
87
+ (x.clean_downcased_title =~ /statement.*of.*earnings/) ||
88
+ (x.clean_downcased_title =~ /statement.*of.*income/) ||
89
+ (x.clean_downcased_title =~ /income.*statement/)) &&
90
+ (x.clean_downcased_title =~ /comprehensive/) &&
91
+ !(x.clean_downcased_title =~ /and other comprehensive/) &&
92
+ !(x.clean_downcased_title =~ /statement of stockholders equity/) }
93
+ if inc_stmt.nil?
94
+ raise InvalidFilingError.new("Couldn't find comprehensive income statement in: " + calculations.map{ |x| "\"#{x.clean_downcased_title}\"" }.join("; "))
95
+ end
96
+
97
+ @comprehensive_income_stmt = ComprehensiveIncomeStatementCalculation.new(inc_stmt)
98
+ end
99
+ return @comprehensive_income_stmt
100
+ end
101
+
102
+ def has_a_comprehensive_income_statement?
103
+ begin
104
+ return comprehensive_income_statement ? true : false
105
+ rescue
106
+ return false
107
+ end
108
+ end
109
+
68
110
  def cash_flow_statement
69
111
  if @cash_flow_stmt.nil?
70
112
  calculations=@taxonomy.callb.calculation
71
- cash_flow_stmt = calculations.find{ |x| (x.clean_downcased_title =~ /statement.*cash.*flows/) or
72
- (x.clean_downcased_title =~ /^cash flows$/) }
113
+ cash_flow_stmt = calculations.find{ |x| (x.clean_downcased_title =~ /statement.*cash.*flow(|s)/) ||
114
+ (x.clean_downcased_title =~ /^cash flow(|s)$/) }
73
115
  if cash_flow_stmt.nil?
74
- raise RuntimeError.new("Couldn't find cash flow statement in: " + calculations.map{ |x| "\"#{x.clean_downcased_title}\"" }.join("; "))
116
+ raise InvalidFilingError.new("Couldn't find cash flow statement in: " + calculations.map{ |x| "\"#{x.clean_downcased_title}\"" }.join("; "))
75
117
  end
76
118
 
77
119
  @cash_flow_stmt = CashFlowStatementCalculation.new(cash_flow_stmt)
@@ -79,14 +121,51 @@ module FinModeling
79
121
  return @cash_flow_stmt
80
122
  end
81
123
 
124
+ def has_a_shareholder_equity_statement?
125
+ #puts "calculations: " + @taxonomy.callb.calculation.map{ |x| x.clean_downcased_title }.join(',')
126
+ begin
127
+ return !shareholder_equity_statement.nil?
128
+ rescue
129
+ return false
130
+ end
131
+ end
132
+
133
+ def shareholder_equity_statement
134
+ if @shareholder_equity_stmt.nil?
135
+ calculations=@taxonomy.callb.calculation
136
+ shareholder_equity_stmt = calculations.find{ |x| (x.clean_downcased_title =~ /statement(|s).*of.*(share|stock)holders(|').*equity(|.*and.*comprehensive|.*and.*other.*comprehensive|.*and.*comprehensive)(|.*income|.*loss|.*income.*loss|.*loss.*income)$/) ||
137
+ (x.clean_downcased_title =~ /statements.*of.*changes.*in.*shareholders.*equity/) }
138
+ if shareholder_equity_stmt.nil?
139
+ raise InvalidFilingError.new("Couldn't find shareholders' equity statement in: " + calculations.map{ |x| "\"#{x.clean_downcased_title}\"" }.join("; "))
140
+ end
141
+
142
+ @shareholder_equity_stmt = ShareholderEquityStatementCalculation.new(shareholder_equity_stmt)
143
+ end
144
+ return @shareholder_equity_stmt
145
+ end
146
+
82
147
  def is_valid?
83
- return (income_statement.is_valid? and balance_sheet.is_valid? and cash_flow_statement.is_valid?)
148
+ puts "balance sheet is not valid" if !balance_sheet.is_valid?
149
+ puts "income statment is not valid" if has_an_income_statement? && !income_statement.is_valid?
150
+ puts "comprehensive income statment is not valid" if has_a_comprehensive_income_statement? && !comprehensive_income_statement.is_valid?
151
+ #puts "cash flow statement is not valid" if !cash_flow_statement.is_valid?
152
+
153
+ return false if !balance_sheet.is_valid?
154
+ return false if has_an_income_statement? && !income_statement.is_valid?
155
+ return false if has_a_comprehensive_income_statement? && !comprehensive_income_statement.is_valid?
156
+ #return false if !cash_flow_statement.is_valid? # FIXME: why can't we enable this?
157
+ return true
84
158
  end
85
159
 
86
160
  def write_constructor(file, item_name)
87
- balance_sheet.write_constructor( file, bs_name = item_name + "_bs")
88
- income_statement.write_constructor( file, is_name = item_name + "_is")
89
- cash_flow_statement.write_constructor(file, cfs_name = item_name + "_cfs")
161
+ balance_sheet .write_constructor(file, bs_name = item_name + "_bs" )
162
+ income_statement .write_constructor(file, is_name = item_name + "_is" ) if has_an_income_statement?
163
+ comprehensive_income_statement.write_constructor(file, cis_name = item_name + "_cis") if has_a_comprehensive_income_statement?
164
+ cash_flow_statement .write_constructor(file, cfs_name = item_name + "_cfs")
165
+ shareholder_equity_statement .write_constructor(file, ses_name = item_name + "_ses") if has_a_shareholder_equity_statement?
166
+ is_name = "nil" if !has_an_income_statement?
167
+ cis_name = "nil" if !has_a_comprehensive_income_statement?
168
+ ses_name = "nil" if !has_a_shareholder_equity_statement?
90
169
 
91
170
  names_of_discs = []
92
171
  disclosures.each_with_index do |disclosure, idx|
@@ -98,7 +177,7 @@ module FinModeling
98
177
 
99
178
  file.puts "#{SCHEMA_VERSION_ITEM} = #{CURRENT_SCHEMA_VERSION}"
100
179
 
101
- file.puts "#{item_name} = FinModeling::CachedAnnualFiling.new(#{bs_name}, #{is_name}, #{cfs_name}, #{names_of_discs_str})"
180
+ file.puts "#{item_name} = FinModeling::CachedAnnualFiling.new(#{bs_name}, #{is_name}, #{cis_name}, #{cfs_name}, #{ses_name}, #{names_of_discs_str})"
102
181
  end
103
182
  end
104
183
  end
File without changes
@@ -17,9 +17,6 @@ module FinModeling
17
17
  thesummary = lookup_cached_summary(summary_cache_key)
18
18
  return thesummary if !thesummary.nil?
19
19
 
20
- mapping = Xbrlware::ValueMapping.new
21
- mapping.policy[:credit] = :flip
22
-
23
20
  thesummary = super(:period => args[:period], :mapping => mapping)
24
21
  if !lookup_cached_classifications(BASE_FILENAME, thesummary.rows)
25
22
  lookahead = [4, thesummary.rows.length-1].min
@@ -32,5 +29,17 @@ module FinModeling
32
29
  return thesummary
33
30
  end
34
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
+
35
44
  end
36
45
  end
File without changes
File without changes
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module FinModeling
2
4
 
3
5
  class BalanceSheetAnalyses < CalculationSummary
@@ -11,19 +13,32 @@ module FinModeling
11
13
  @totals_row_enabled = false
12
14
  end
13
15
 
14
- def print_extras # FIXME: rename
16
+ def print_regressions # FIXME: rename
15
17
  lr = noa_growth_row.valid_vals.linear_regression
16
- puts "\t\tNOA growth: "+
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: "+
17
27
  "a:#{lr.a.to_s.cap_decimals(4)}, "+
18
28
  "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)}"
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") )
21
32
  end
22
33
 
23
34
  def noa_growth_row
24
35
  find_row_by_key('NOA Growth')
25
36
  end
26
37
 
38
+ def composition_ratio_row
39
+ find_row_by_key('Composition Ratio')
40
+ end
41
+
27
42
  def find_row_by_key(key) # FIXME: move this to CalculationSummary
28
43
  self.rows.find{ |x| x.key == key }
29
44
  end
@@ -1,60 +1,58 @@
1
1
  module FinModeling
2
2
  class BalanceSheetCalculation < CompanyFilingCalculation
3
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]+/ ]
4
9
  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)
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
13
15
  end
14
- return @assets
15
16
  end
16
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...
17
22
  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)
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
25
28
  end
26
- return @liabs_and_equity
27
29
  end
28
30
 
29
31
  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
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
36
35
 
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
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
41
44
  end
45
+ puts "calculation tree:\n" + self.calculation.sprint_tree(indent_count=0, simplified=true)
42
46
  end
43
47
 
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)
48
+ return (assets_calculation.has_cash_item &&
49
+ liabs_and_equity_calculation.has_equity_item &&
50
+ is_balanced)
53
51
  end
54
52
 
55
53
  def reformulated(period)
56
54
  return ReformulatedBalanceSheet.new(period,
57
- assets_calculation.summary(:period=>period),
55
+ assets_calculation .summary(:period=>period),
58
56
  liabs_and_equity_calculation.summary(:period=>period))
59
57
  end
60
58
 
@@ -64,5 +62,22 @@ module FinModeling
64
62
  file.puts "#{item_name} = FinModeling::BalanceSheetCalculation.new(#{item_calc_name})"
65
63
  end
66
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
+
67
82
  end
68
83
  end
@@ -10,11 +10,40 @@ module FinModeling
10
10
  end
11
11
 
12
12
  def valid_vals
13
- ArrayWithStats.new(@vals.select{ |val| !val.nil? })
13
+ ArrayWithStats.new(@vals.select{ |val| !val.nil? &&
14
+ !val.is_a?(Complex) &&
15
+ (!val.is_a?(Float) || (!val.nan? && val.finite?))})
16
+ end
17
+
18
+ def num_vals
19
+ @vals.length
20
+ end
21
+
22
+ def min_abs_val
23
+ @vals.map{ |x| x.abs }.min
24
+ end
25
+
26
+ def scale_down_by(val)
27
+ if val == :thousand
28
+ @vals.map!{ |val| val / 1000.0 }
29
+ @key += " ($KK)"
30
+ elsif val == :million
31
+ @vals.map!{ |val| val / 1000000.0 }
32
+ @key += " ($MM)"
33
+ else
34
+ raise RuntimeError.new("Bogus val: #{val}")
35
+ end
36
+ end
37
+
38
+ def insert_column_before(col_idx, val)
39
+ @vals.insert(col_idx, val)
14
40
  end
15
41
 
16
42
  def print(key_width=18, max_decimals=4, val_width=12)
17
- justified_key = @key.fixed_width_left_justify(key_width)
43
+ type_and_key = ""
44
+ type_and_key += "[#{@type}] " if @type
45
+ type_and_key += @key
46
+ key_lines = type_and_key.split_into_lines_shorter_than(key_width).map{ |line| line.fixed_width_left_justify(key_width) }
18
47
 
19
48
  justified_vals = ""
20
49
  @vals.each do |val|
@@ -22,7 +51,10 @@ module FinModeling
22
51
  justified_vals += " " + val_with_commas.cap_decimals(max_decimals).fixed_width_right_justify(val_width)
23
52
  end
24
53
 
25
- puts "\t" + justified_key + justified_vals
54
+ puts "\t" + key_lines.shift + justified_vals
55
+ key_lines.each do |line|
56
+ puts "\t " + line
57
+ end
26
58
  end
27
59
 
28
60
  def write_constructor(file, item_name)
@@ -63,10 +95,25 @@ module FinModeling
63
95
  @val_width = 12
64
96
  @max_decimals = 4
65
97
  @totals_row_enabled = true
98
+ @rows = [ ]
66
99
  end
67
100
 
68
101
  def num_value_columns
69
- @rows.map{ |row| row.vals.length }.max
102
+ @rows.map{ |row| row.num_vals }.max
103
+ end
104
+
105
+ def auto_scale!
106
+ min_val = @rows.map{ |row| row.min_abs_val }.min
107
+ if min_val >= 1000 && min_val < 100000
108
+ @rows.each { |row| row.scale_down_by(:thousand) }
109
+ elsif min_val >= 1000000
110
+ @rows.each { |row| row.scale_down_by(:million) }
111
+ end
112
+ end
113
+
114
+ def insert_column_before(col_idx, val=nil)
115
+ @header_row.insert_column_before(col_idx, val) if @header_row
116
+ @rows.each{ |row| row.insert_column_before(col_idx, val) }
70
117
  end
71
118
 
72
119
  def total(col_idx=0)
@@ -120,24 +167,82 @@ module FinModeling
120
167
  file.puts "#{item_name}.rows = [#{row_item_names.join(',')}]"
121
168
  end
122
169
 
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
-
170
+ def +(other)
171
+ raise RuntimeError.new("can't add a CalculationSummary to a #{other.class}") if !other.is_a?(CalculationSummary)
127
172
  multics = CalculationSummary.new
128
173
  multics.title = @title
174
+ multics.rows = []
129
175
 
130
176
  if @header_row
131
177
  multics.header_row = CalculationHeader.new(
132
178
  :key => @header_row.key.dup,
133
- :vals => @header_row.vals.dup + mccs.header_row.vals.dup)
179
+ :vals => @header_row.vals.dup + other.header_row.vals.dup)
134
180
  end
135
181
 
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)
182
+ myrows = @rows.dup
183
+ itsrows = other.rows.dup
184
+ while myrows.any? || itsrows.any?
185
+ new_row = CalculationRow.new( :key => "", :vals => [] )
186
+
187
+ if (myrows.any? && itsrows.empty?)
188
+ new_row.key = myrows.first.key.dup
189
+ new_row.vals += myrows.first.vals.dup
190
+ new_row.vals += [""]*other.num_value_column
191
+
192
+ myrows.shift
193
+
194
+ elsif (myrows.empty? && itsrows.any?)
195
+ new_row.key = itsrows.first.key.dup
196
+ new_row.vals += [""]*num_value_columns
197
+ new_row.vals += itsrows.first.vals.dup
198
+
199
+ itsrows.shift
200
+
201
+ elsif (myrows.first.key == itsrows.first.key)
202
+ new_row.key = myrows.first.key.dup
203
+ new_row.vals += myrows.first.vals.dup
204
+ new_row.vals += itsrows.first.vals.dup
205
+
206
+ myrows.shift
207
+ itsrows.shift
208
+
209
+ elsif (myrows.first.key < itsrows.first.key)
210
+ if myrow=myrows.find{|row| row.key == itsrows.first.key }
211
+ new_row.key = myrows.first.key.dup
212
+ new_row.vals += myrows.first.vals.dup
213
+ new_row.vals += itsrows.first.vals.dup
214
+
215
+ myrows.delete(myrow)
216
+ itsrows.shift
217
+
218
+ else
219
+ new_row.key = itsrows.first.key.dup
220
+ new_row.vals += [""]*num_value_columns
221
+ new_row.vals += itsrows.first.vals.dup
222
+
223
+ itsrows.shift
224
+ end
225
+
226
+ elsif (myrows.first.key > itsrows.first.key)
227
+ if itsrow=itsrows.find{|row| row.key == myrows.first.key }
228
+ new_row.key = myrows.first.key.dup
229
+ new_row.vals += myrows.first.vals.dup
230
+ new_row.vals += itsrows.first.vals.dup
231
+
232
+ myrows.shift
233
+ itsrows.delete(itsrow)
234
+
235
+ else
236
+ new_row.key = myrows.first.key.dup
237
+ new_row.vals += myrows.first.vals.dup
238
+ new_row.vals += [""]*other.num_value_columns
239
+
240
+ myrows.shift
241
+ end
242
+
243
+ end
244
+
245
+ multics.rows << new_row
141
246
  end
142
247
 
143
248
  return multics
File without changes
File without changes
@@ -0,0 +1,15 @@
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
File without changes
@@ -0,0 +1,80 @@
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
+ common_dates = index_quotes .year_and_month_strings &
35
+ company_quotes.year_and_month_strings
36
+
37
+ index_quotes .filter_by_date!(common_dates)
38
+ company_quotes.filter_by_date!(common_dates)
39
+
40
+ index_div_hist = NasdaqQuery::DividendHistory.for_symbol(index_ticker)
41
+ company_div_hist = NasdaqQuery::DividendHistory.for_symbol(company_ticker)
42
+
43
+ index_monthly_returns = index_quotes .monthly_returns(index_div_hist)
44
+ company_monthly_returns = company_quotes.monthly_returns(company_div_hist)
45
+
46
+ x = GSL::Vector.alloc(index_monthly_returns)
47
+ y = GSL::Vector.alloc(company_monthly_returns)
48
+ intercept, slope, cov00, cov01, cov11, chisq, status = GSL::Fit::linear(x, y)
49
+
50
+ # FIXME: evaluate [intercept - Rf*(1-beta)]. It tells how much better/worse than expected (given its risk) the stock did. [per time period]
51
+
52
+ # FIXME: subtracting/adding one standard error of the beta gives a 95% confidence interval. That could be used to give a confidence interval
53
+ # for the resulting valuation.
54
+
55
+ beta = slope
56
+ end
57
+ end
58
+
59
+ class AdjustedBeta # see: http://financetrain.com/adjusted-and-unadjusted-beta/
60
+ MEAN_LONG_TERM_BETA = 1.0
61
+ def self.from_beta(raw_beta)
62
+ ((2.0*raw_beta) + (1.0*MEAN_LONG_TERM_BETA)) / 3.0
63
+ end
64
+ end
65
+
66
+ class EquityCostOfCapital
67
+ def self.from_beta(beta)
68
+ Rate.new(RiskFreeRate.forward_estimate.value + (beta * MARKET_PREMIUM))
69
+ end
70
+
71
+ def self.from_ticker(company_ticker)
72
+ raw_beta = Beta.from_ticker(company_ticker)
73
+ puts "CAPM::EquityCostOfCapital -> raw beta = #{raw_beta}"
74
+ adj_beta = AdjustedBeta.from_beta(raw_beta)
75
+ puts "CAPM::EquityCostOfCapital -> adj beta = #{adj_beta}"
76
+ self.from_beta(adj_beta)
77
+ end
78
+ end
79
+ end
80
+ end