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