finmodeling 0.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +0 -0
- data/Gemfile +2 -0
- data/README.md +289 -269
- data/Rakefile +12 -0
- data/TODO.txt +113 -20
- data/examples/{dump_report.rb → dump_latest_10k.rb} +1 -1
- data/examples/list_disclosures.rb +50 -0
- data/examples/lists/nasdaq-mid-to-mega-tech-symbols.txt +0 -0
- data/examples/show_report.rb +112 -32
- data/examples/show_reports.rb +162 -33
- data/finmodeling.gemspec +4 -1
- data/lib/finmodeling/annual_report_filing.rb +97 -18
- data/lib/finmodeling/array_with_stats.rb +0 -0
- data/lib/finmodeling/assets_calculation.rb +12 -3
- data/lib/finmodeling/assets_item.rb +0 -0
- data/lib/finmodeling/assets_item_vectors.rb +0 -0
- data/lib/finmodeling/balance_sheet_analyses.rb +19 -4
- data/lib/finmodeling/balance_sheet_calculation.rb +52 -37
- data/lib/finmodeling/calculation_summary.rb +119 -14
- data/lib/finmodeling/can_cache_classifications.rb +0 -0
- data/lib/finmodeling/can_cache_summaries.rb +0 -0
- data/lib/finmodeling/can_choose_successive_periods.rb +15 -0
- data/lib/finmodeling/can_classify_rows.rb +0 -0
- data/lib/finmodeling/capm.rb +80 -0
- data/lib/finmodeling/cash_change_calculation.rb +3 -3
- data/lib/finmodeling/cash_change_item.rb +0 -0
- data/lib/finmodeling/cash_change_item_vectors.rb +0 -0
- data/lib/finmodeling/cash_change_summary_from_differences.rb +36 -0
- data/lib/finmodeling/cash_flow_statement_analyses.rb +36 -0
- data/lib/finmodeling/cash_flow_statement_calculation.rb +28 -52
- data/lib/finmodeling/classifiers.rb +2 -0
- data/lib/finmodeling/company.rb +0 -0
- data/lib/finmodeling/company_filing.rb +30 -7
- data/lib/finmodeling/company_filing_calculation.rb +16 -6
- data/lib/finmodeling/company_filings.rb +112 -46
- data/lib/finmodeling/comprehensive_income_calculation.rb +60 -0
- data/lib/finmodeling/comprehensive_income_statement_calculation.rb +74 -0
- data/lib/finmodeling/comprehensive_income_statement_item.rb +20 -0
- data/lib/finmodeling/comprehensive_income_statement_item_vectors.rb +235 -0
- data/lib/finmodeling/config.rb +0 -0
- data/lib/finmodeling/debt_cost_of_capital.rb +14 -0
- data/lib/finmodeling/equity_change_calculation.rb +43 -0
- data/lib/finmodeling/equity_change_item.rb +25 -0
- data/lib/finmodeling/equity_change_item_vectors.rb +156 -0
- data/lib/finmodeling/factory.rb +0 -0
- data/lib/finmodeling/fama_french_cost_of_equity.rb +119 -0
- data/lib/finmodeling/float_helpers.rb +14 -8
- data/lib/finmodeling/forecasted_reformulated_balance_sheet.rb +55 -0
- data/lib/finmodeling/forecasted_reformulated_income_statement.rb +110 -0
- data/lib/finmodeling/forecasts.rb +4 -4
- data/lib/finmodeling/has_string_classifer.rb +0 -0
- data/lib/finmodeling/income_statement_analyses.rb +23 -17
- data/lib/finmodeling/income_statement_calculation.rb +46 -43
- data/lib/finmodeling/income_statement_item.rb +1 -1
- data/lib/finmodeling/income_statement_item_vectors.rb +24 -13
- data/lib/finmodeling/invalid_filing_error.rb +4 -0
- data/lib/finmodeling/liabs_and_equity_calculation.rb +18 -8
- data/lib/finmodeling/liabs_and_equity_item.rb +1 -1
- data/lib/finmodeling/liabs_and_equity_item_vectors.rb +24 -24
- data/lib/finmodeling/linear_trend_forecasting_policy.rb +23 -0
- data/lib/finmodeling/net_income_calculation.rb +23 -10
- data/lib/finmodeling/net_income_summary_from_differences.rb +51 -0
- data/lib/finmodeling/paths.rb +0 -0
- data/lib/finmodeling/period_array.rb +8 -4
- data/lib/finmodeling/quarterly_report_filing.rb +9 -4
- data/lib/finmodeling/rate.rb +8 -0
- data/lib/finmodeling/ratio.rb +0 -0
- data/lib/finmodeling/reformulated_balance_sheet.rb +47 -88
- data/lib/finmodeling/reformulated_cash_flow_statement.rb +18 -41
- data/lib/finmodeling/reformulated_income_statement.rb +44 -206
- data/lib/finmodeling/reformulated_shareholder_equity_statement.rb +50 -0
- data/lib/finmodeling/reoi_valuation.rb +104 -0
- data/lib/finmodeling/shareholder_equity_statement_calculation.rb +34 -0
- data/lib/finmodeling/string_helpers.rb +18 -1
- data/lib/finmodeling/time_series_estimator.rb +25 -0
- data/lib/finmodeling/trailing_avg_forecasting_policy.rb +23 -0
- data/lib/finmodeling/version.rb +1 -1
- data/lib/finmodeling/weighted_avg_cost_of_capital.rb +35 -0
- data/lib/finmodeling/yahoo_finance_helpers.rb +20 -0
- data/lib/finmodeling.rb +33 -2
- data/spec/annual_report_filing_spec.rb +81 -45
- data/spec/assets_calculation_spec.rb +7 -4
- data/spec/assets_item_spec.rb +9 -14
- data/spec/balance_sheet_analyses_spec.rb +13 -13
- data/spec/balance_sheet_calculation_spec.rb +45 -51
- data/spec/calculation_summary_spec.rb +113 -21
- data/spec/can_classify_rows_spec.rb +0 -0
- data/spec/cash_change_calculation_spec.rb +1 -10
- data/spec/cash_change_item_spec.rb +10 -18
- data/spec/cash_flow_statement_calculation_spec.rb +10 -24
- data/spec/company_beta_spec.rb +53 -0
- data/spec/company_filing_calculation_spec.rb +39 -49
- data/spec/company_filing_spec.rb +0 -0
- data/spec/company_filings_spec.rb +75 -25
- data/spec/company_spec.rb +37 -47
- data/spec/comprehensive_income_statement_calculation_spec.rb +54 -0
- data/spec/comprehensive_income_statement_item_spec.rb +56 -0
- data/spec/debt_cost_of_capital_spec.rb +19 -0
- data/spec/equity_change_calculation_spec.rb +33 -0
- data/spec/equity_change_item_spec.rb +58 -0
- data/spec/factory_spec.rb +2 -2
- data/spec/forecasts_spec.rb +2 -2
- data/spec/income_statement_analyses_spec.rb +23 -21
- data/spec/income_statement_calculation_spec.rb +17 -49
- data/spec/income_statement_item_spec.rb +17 -29
- data/spec/liabs_and_equity_calculation_spec.rb +6 -3
- data/spec/liabs_and_equity_item_spec.rb +14 -22
- data/spec/linear_trend_forecasting_policy_spec.rb +37 -0
- data/spec/matchers/custom_matchers.rb +79 -0
- data/spec/mocks/calculation.rb +0 -0
- data/spec/mocks/income_statement_analyses.rb +0 -0
- data/spec/mocks/sec_query.rb +0 -0
- data/spec/net_income_calculation_spec.rb +16 -10
- data/spec/period_array.rb +0 -0
- data/spec/quarterly_report_filing_spec.rb +21 -38
- data/spec/rate_spec.rb +0 -0
- data/spec/ratio_spec.rb +0 -0
- data/spec/reformulated_balance_sheet_spec.rb +56 -33
- data/spec/reformulated_cash_flow_statement_spec.rb +18 -10
- data/spec/reformulated_income_statement_spec.rb +16 -15
- data/spec/reformulated_shareholder_equity_statement_spec.rb +43 -0
- data/spec/reoi_valuation_spec.rb +146 -0
- data/spec/shareholder_equity_statement_calculation_spec.rb +59 -0
- data/spec/spec_helper.rb +4 -1
- data/spec/string_helpers_spec.rb +15 -13
- data/spec/time_series_estimator_spec.rb +61 -0
- data/spec/trailing_avg_forecasting_policy_spec.rb +37 -0
- data/spec/weighted_avg_cost_of_capital_spec.rb +32 -0
- data/tools/create_equity_change_training_vectors.rb +49 -0
- data/tools/time_specs.sh +7 -0
- metadata +182 -36
- data/lib/finmodeling/constant_forecasting_policy.rb +0 -23
- data/lib/finmodeling/generic_forecasting_policy.rb +0 -19
- data/spec/constant_forecasting_policy_spec.rb +0 -37
- 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.
|
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/)
|
41
|
-
|
42
|
-
|
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
|
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/)
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
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.*
|
72
|
-
(x.clean_downcased_title =~ /^cash
|
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
|
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
|
-
|
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(
|
88
|
-
income_statement.write_constructor(
|
89
|
-
|
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
|
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
|
20
|
-
"
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
31
|
-
|
32
|
-
|
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
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
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" +
|
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.
|
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 +(
|
124
|
-
raise RuntimeError.new("can't add a CalculationSummary to a #{
|
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 +
|
179
|
+
:vals => @header_row.vals.dup + other.header_row.vals.dup)
|
134
180
|
end
|
135
181
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|