finmodeling 0.1 → 0.2

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 (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