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
|
@@ -14,7 +14,7 @@ describe FinModeling::ReformulatedIncomeStatement do
|
|
|
14
14
|
|
|
15
15
|
@inc_stmt_2009 = @filing_2009.income_statement
|
|
16
16
|
is_period_2009 = @inc_stmt_2009.periods.last
|
|
17
|
-
@reformed_inc_stmt_2009 = @inc_stmt_2009.reformulated(is_period_2009)
|
|
17
|
+
@reformed_inc_stmt_2009 = @inc_stmt_2009.reformulated(is_period_2009, ci_calc=nil)
|
|
18
18
|
|
|
19
19
|
@bal_sheet_2009 = @filing_2009.balance_sheet
|
|
20
20
|
bs_period_2009 = @bal_sheet_2009.periods.last
|
|
@@ -22,7 +22,7 @@ describe FinModeling::ReformulatedIncomeStatement do
|
|
|
22
22
|
|
|
23
23
|
@inc_stmt_2011 = @filing_2011.income_statement
|
|
24
24
|
is_period_2011 = @inc_stmt_2011.periods.last
|
|
25
|
-
@reformed_inc_stmt_2011 = @inc_stmt_2011.reformulated(is_period_2011)
|
|
25
|
+
@reformed_inc_stmt_2011 = @inc_stmt_2011.reformulated(is_period_2011, ci_calc=nil)
|
|
26
26
|
|
|
27
27
|
@bal_sheet_2011 = @filing_2011.balance_sheet
|
|
28
28
|
bs_period_2011 = @bal_sheet_2011.periods.last
|
|
@@ -171,7 +171,7 @@ describe FinModeling::ReformulatedIncomeStatement do
|
|
|
171
171
|
end
|
|
172
172
|
|
|
173
173
|
describe "analysis" do
|
|
174
|
-
subject {@reformed_inc_stmt_2011.analysis(@reformed_bal_sheet_2011, @reformed_inc_stmt_2009, @reformed_bal_sheet_2009) }
|
|
174
|
+
subject {@reformed_inc_stmt_2011.analysis(@reformed_bal_sheet_2011, @reformed_inc_stmt_2009, @reformed_bal_sheet_2009, e_ror=0.10) }
|
|
175
175
|
|
|
176
176
|
it { should be_an_instance_of FinModeling::CalculationSummary }
|
|
177
177
|
it "contains the expected rows" do
|
|
@@ -191,7 +191,7 @@ describe FinModeling::ReformulatedIncomeStatement do
|
|
|
191
191
|
|
|
192
192
|
@inc_stmt_2011_q3 = @filing_2011_q3.income_statement
|
|
193
193
|
is_period_2011_q3 = @inc_stmt_2011_q3.periods.threequarterly.last
|
|
194
|
-
@reformed_inc_stmt_2011_q3 = @inc_stmt_2011_q3.reformulated(is_period_2011_q3)
|
|
194
|
+
@reformed_inc_stmt_2011_q3 = @inc_stmt_2011_q3.reformulated(is_period_2011_q3, ci_calc=nil)
|
|
195
195
|
|
|
196
196
|
@diff = @reformed_inc_stmt_2011 - @reformed_inc_stmt_2011_q3
|
|
197
197
|
end
|
|
@@ -248,7 +248,8 @@ describe FinModeling::ReformulatedIncomeStatement do
|
|
|
248
248
|
before (:all) do
|
|
249
249
|
@company = FinModeling::Company.find("aapl")
|
|
250
250
|
@filings = FinModeling::CompanyFilings.new(@company.filings_since_date(Time.parse("2010-10-01")))
|
|
251
|
-
@
|
|
251
|
+
@last_operating_revenues = @filings.last.income_statement.latest_quarterly_reformulated(nil, nil, nil).operating_revenues.total
|
|
252
|
+
@policy = FinModeling::GenericForecastingPolicy.new(:operating_revenues => @last_operating_revenues)
|
|
252
253
|
|
|
253
254
|
prev_bs_period = @filings.last.balance_sheet.periods.last
|
|
254
255
|
next_bs_period_value = prev_bs_period.value.next_month.next_month.next_month
|
|
@@ -260,7 +261,7 @@ describe FinModeling::ReformulatedIncomeStatement do
|
|
|
260
261
|
end
|
|
261
262
|
|
|
262
263
|
let(:last_re_bs) { @filings.last.balance_sheet.reformulated(@filings.last.balance_sheet.periods.last) }
|
|
263
|
-
let(:last_re_is) { @filings.last.income_statement.latest_quarterly_reformulated(nil) }
|
|
264
|
+
let(:last_re_is) { @filings.last.income_statement.latest_quarterly_reformulated(nil, nil, nil) }
|
|
264
265
|
let(:next_re_is) { FinModeling::ReformulatedIncomeStatement.forecast_next(@next_is_period, @policy, last_re_bs, last_re_is) }
|
|
265
266
|
let(:next_re_bs) { FinModeling::ReformulatedBalanceSheet.forecast_next(@next_bs_period, @policy, last_re_bs, next_re_is) }
|
|
266
267
|
|
|
@@ -270,24 +271,24 @@ describe FinModeling::ReformulatedIncomeStatement do
|
|
|
270
271
|
it "should have the given period" do
|
|
271
272
|
subject.period.to_pretty_s == @next_is_period.to_pretty_s
|
|
272
273
|
end
|
|
273
|
-
it "should set operating_revenue to last year's revenue
|
|
274
|
-
expected_val = last_re_is.operating_revenues.total
|
|
275
|
-
subject.operating_revenues.total.should
|
|
274
|
+
it "should set operating_revenue to last year's revenue" do
|
|
275
|
+
expected_val = last_re_is.operating_revenues.total
|
|
276
|
+
subject.operating_revenues.total.should be_within(0.1).of(expected_val)
|
|
276
277
|
end
|
|
277
278
|
it "should set OISAT to operating revenue times sales PM" do
|
|
278
|
-
expected_val = subject.operating_revenues.total * @policy.
|
|
279
|
-
subject.income_from_sales_after_tax.total.should
|
|
279
|
+
expected_val = subject.operating_revenues.total * @policy.sales_pm_on(@next_is_period.value["end_date"])
|
|
280
|
+
subject.income_from_sales_after_tax.total.should be_within(0.1).of(expected_val)
|
|
280
281
|
end
|
|
281
282
|
it "should set NFI to fi_over_nfa times last year's NFA" do
|
|
282
|
-
expected_val = last_re_bs.net_financial_assets.total * (@policy.
|
|
283
|
-
subject.net_financing_income.total.should
|
|
283
|
+
expected_val = last_re_bs.net_financial_assets.total * (@policy.fi_over_nfa_on(@next_is_period.value["end_date"])/4) # FIXME use Rate.annualize
|
|
284
|
+
subject.net_financing_income.total.should be_within(0.1).of(expected_val)
|
|
284
285
|
end
|
|
285
286
|
it "should set comprehensive income to OISAT plus NFI" do
|
|
286
287
|
expected_val = subject.income_from_sales_after_tax.total + subject.net_financing_income.total
|
|
287
|
-
subject.comprehensive_income.total.should
|
|
288
|
+
subject.comprehensive_income.total.should be_within(0.1).of(expected_val)
|
|
288
289
|
end
|
|
289
290
|
it "should have an empty analysis (with the same rows)" do
|
|
290
|
-
subject.analysis(next_re_bs, last_re_is, last_re_bs)
|
|
291
|
+
subject.analysis(next_re_bs, last_re_is, last_re_bs, e_ror=0.10)
|
|
291
292
|
end
|
|
292
293
|
end
|
|
293
294
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# reformulated_income_statement_spec.rb
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe FinModeling::ReformulatedShareholderEquityStatement do
|
|
6
|
+
before(:all) do
|
|
7
|
+
deere_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/315189/000110465910063219/0001104659-10-063219-index.htm"
|
|
8
|
+
filing = FinModeling::AnnualReportFiling.download deere_2011_annual_rpt
|
|
9
|
+
stmt = filing.shareholder_equity_statement
|
|
10
|
+
period = stmt.periods.last
|
|
11
|
+
|
|
12
|
+
@equity_chg = stmt.equity_change_calculation.summary(:period => period)
|
|
13
|
+
@re_ses = stmt.reformulated(period)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe "transactions_with_shareholders" do
|
|
17
|
+
subject { @re_ses.transactions_with_shareholders }
|
|
18
|
+
it { should be_a FinModeling::CalculationSummary }
|
|
19
|
+
its(:total) { should be_within(0.1).of( @equity_chg.filter_by_type(:share_issue ).total +
|
|
20
|
+
@equity_chg.filter_by_type(:minority_int ).total +
|
|
21
|
+
@equity_chg.filter_by_type(:share_repurch).total +
|
|
22
|
+
@equity_chg.filter_by_type(:common_div ).total) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe "comprehensive_income" do
|
|
26
|
+
subject { @re_ses.comprehensive_income }
|
|
27
|
+
it { should be_a FinModeling::CalculationSummary }
|
|
28
|
+
its(:total) { should be_within(0.1).of( @equity_chg.filter_by_type(:net_income ).total +
|
|
29
|
+
@equity_chg.filter_by_type(:oci ).total +
|
|
30
|
+
@equity_chg.filter_by_type(:preferred_div).total) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe "analysis" do
|
|
34
|
+
subject { @re_ses.analysis }
|
|
35
|
+
|
|
36
|
+
it { should be_a FinModeling::CalculationSummary }
|
|
37
|
+
it "contains the expected rows" do
|
|
38
|
+
expected_keys = [ "Tx w Shareholders ($MM)", "CI ($MM)" ]
|
|
39
|
+
subject.rows.map{ |row| row.key }.should == expected_keys
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe FinModeling::ReOIValuation do
|
|
4
|
+
before (:all) do
|
|
5
|
+
@company = FinModeling::Company.find("aapl")
|
|
6
|
+
@filings = FinModeling::CompanyFilings.new(@company.filings_since_date(Time.parse("2012-10-01")))
|
|
7
|
+
@cost_of_capital = FinModeling::Rate.new(0.086) # 8.6% (made up)
|
|
8
|
+
@forecasts = @filings.forecasts(@filings.choose_forecasting_policy(@cost_of_capital.value), num_forecast_periods=4)
|
|
9
|
+
@num_shares = 934882640
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe ".new" do
|
|
13
|
+
subject { FinModeling::ReOIValuation.new(@filings, @forecasts, @cost_of_capital, @num_shares) }
|
|
14
|
+
|
|
15
|
+
it { should be_a FinModeling::ReOIValuation }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe ".summary" do
|
|
19
|
+
let(:valuation) { FinModeling::ReOIValuation.new(@filings, @forecasts, @cost_of_capital, @num_shares) }
|
|
20
|
+
subject { valuation.summary }
|
|
21
|
+
|
|
22
|
+
it { should be_a FinModeling::CalculationSummary }
|
|
23
|
+
its(:title) { should == "ReOI Valuation" }
|
|
24
|
+
its(:totals_row_enabled) { should be_false }
|
|
25
|
+
it "should have the right row keys" do
|
|
26
|
+
expected_keys = []
|
|
27
|
+
expected_keys << "ReOI ($MM)"
|
|
28
|
+
expected_keys << "PV(ReOI) ($MM)"
|
|
29
|
+
expected_keys << "CV ($MM)"
|
|
30
|
+
expected_keys << "PV(CV) ($MM)"
|
|
31
|
+
expected_keys << "Book Value of Common Equity ($MM)"
|
|
32
|
+
expected_keys << "Enterprise Value ($MM)"
|
|
33
|
+
expected_keys << "NFA ($MM)"
|
|
34
|
+
expected_keys << "Value of Common Equity ($MM)"
|
|
35
|
+
expected_keys << "# Shares (MM)"
|
|
36
|
+
expected_keys << "Value / Share ($)"
|
|
37
|
+
subject.rows.map{ |x| x.key }.should == expected_keys
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "should show today, plus the forecasted periods" do
|
|
41
|
+
period_strings = []
|
|
42
|
+
period_strings << @filings.re_bs_arr.last.period.to_pretty_s
|
|
43
|
+
period_strings += @forecasts.reformulated_balance_sheets.map{ |x| x.period.to_pretty_s + "E" }
|
|
44
|
+
subject.header_row.vals.should == period_strings
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "should show all forecasted ReOIs" do
|
|
48
|
+
prev_re_bses = [@filings.re_bs_arr.last] + @forecasts.reformulated_balance_sheets[0..-2]
|
|
49
|
+
re_ises = @forecasts.reformulated_income_statements
|
|
50
|
+
re_ois = re_ises.zip(prev_re_bses).map{ |pair| pair[0].re_oi(pair[1], @cost_of_capital.value).to_nearest_million }
|
|
51
|
+
|
|
52
|
+
reoi_row = subject.rows.find{ |x| x.key == "ReOI ($MM)" }
|
|
53
|
+
reoi_row.vals[1..-1].should == re_ois
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "should the present value of the first N-1 ReOI forecasts" do
|
|
57
|
+
reoi_row = subject.rows.find{ |x| x.key == "ReOI ($MM)" }
|
|
58
|
+
pv_reoi_row = subject.rows.find{ |x| x.key == "PV(ReOI) ($MM)" }
|
|
59
|
+
|
|
60
|
+
1.upto(reoi_row.vals.length-2) do |col_idx|
|
|
61
|
+
days_from_now = valuation.periods[col_idx].value - Date.today
|
|
62
|
+
discount_rate = FinModeling::Rate.new(@cost_of_capital.value + 1.0)
|
|
63
|
+
d = discount_rate.annualize(from_days=365, to_days=days_from_now)
|
|
64
|
+
expected_pv_reoi = reoi_row.vals[col_idx] / d
|
|
65
|
+
pv_reoi_row.vals[col_idx].should be_within(100.0).of(expected_pv_reoi) # rounding error bc the test is working off of nearest-million values
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "should the continuing value, based on the last period's ReOI" do
|
|
70
|
+
reoi_row = subject.rows.find{ |x| x.key == "ReOI ($MM)" }
|
|
71
|
+
cv_row = subject.rows.find{ |x| x.key == "CV ($MM)" }
|
|
72
|
+
|
|
73
|
+
expected_cv = reoi_row.vals.last / @cost_of_capital.value # FIXME: this is an assumption of zero-growth CV
|
|
74
|
+
cv_row.vals[-2].should be_within(10.0).of(expected_cv)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "should the present value of the continuing value" do
|
|
78
|
+
cv_row = subject.rows.find{ |x| x.key == "CV ($MM)" }
|
|
79
|
+
pv_cv_row = subject.rows.find{ |x| x.key == "PV(CV) ($MM)" }
|
|
80
|
+
|
|
81
|
+
1.upto(cv_row.vals.length-2) do |col_idx|
|
|
82
|
+
if cv_row.vals[col_idx]
|
|
83
|
+
days_from_now = valuation.periods[col_idx].value - Date.today
|
|
84
|
+
discount_rate = FinModeling::Rate.new(@cost_of_capital.value + 1.0)
|
|
85
|
+
d = discount_rate.annualize(from_days=365, to_days=days_from_now)
|
|
86
|
+
expected_pv_cv = cv_row.vals[col_idx] / d
|
|
87
|
+
pv_cv_row.vals[col_idx].should be_within(2.0).of(expected_pv_cv)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "should show the current book value of equity" do
|
|
93
|
+
bv_cse_row = subject.rows.find{ |x| x.key == "Book Value of Common Equity ($MM)" }
|
|
94
|
+
|
|
95
|
+
bv_cse_row.vals[0].should be_within(1.0).of(@filings.re_bs_arr.last.common_shareholders_equity.total.to_nearest_million)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "should show the enterprise value" do
|
|
99
|
+
pv_reoi_row = subject.rows.find{ |x| x.key == "PV(ReOI) ($MM)" }
|
|
100
|
+
pv_cv_row = subject.rows.find{ |x| x.key == "PV(CV) ($MM)" }
|
|
101
|
+
bv_cse_row = subject.rows.find{ |x| x.key == "Book Value of Common Equity ($MM)" }
|
|
102
|
+
ev_row = subject.rows.find{ |x| x.key == "Enterprise Value ($MM)" }
|
|
103
|
+
|
|
104
|
+
expected_ev = pv_reoi_row.vals[1..-2].inject(:+) + pv_cv_row.vals.select{ |x| x }.inject(:+) + bv_cse_row.vals.select{ |x| x }.inject(:+)
|
|
105
|
+
|
|
106
|
+
ev_row.vals[0].should be_within(2.0).of(expected_ev)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "should show the book value of net financial assets" do
|
|
110
|
+
bv_nfa_row = subject.rows.find{ |x| x.key == "NFA ($MM)" }
|
|
111
|
+
|
|
112
|
+
bv_nfa_row.vals[0].should be_within(1.0).of(@filings.re_bs_arr.last.net_financial_assets.total.to_nearest_million)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "should show the value of common equity" do
|
|
116
|
+
ev_row = subject.rows.find{ |x| x.key == "Enterprise Value ($MM)" }
|
|
117
|
+
bf_nfa_row = subject.rows.find{ |x| x.key == "NFA ($MM)" }
|
|
118
|
+
cse_row = subject.rows.find{ |x| x.key == "Value of Common Equity ($MM)" }
|
|
119
|
+
|
|
120
|
+
expected_cse = ev_row.vals[0] + bf_nfa_row.vals[0]
|
|
121
|
+
|
|
122
|
+
cse_row.vals[0].should be_within(1.0).of(expected_cse)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "should show the book value of net financial assets" do
|
|
126
|
+
num_shares_row = subject.rows.find{ |x| x.key == "# Shares (MM)" }
|
|
127
|
+
|
|
128
|
+
num_shares_row.vals[0].should be_within(1.0).of(@num_shares.to_nearest_million)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "should show the value per share" do
|
|
132
|
+
cse_row = subject.rows.find{ |x| x.key == "Value of Common Equity ($MM)" }
|
|
133
|
+
num_shares_row = subject.rows.find{ |x| x.key == "# Shares (MM)" }
|
|
134
|
+
value_per_share_row = subject.rows.find{ |x| x.key == "Value / Share ($)" }
|
|
135
|
+
|
|
136
|
+
expected_value_per_share = cse_row.vals[0] / num_shares_row.vals[0]
|
|
137
|
+
|
|
138
|
+
value_per_share_row.vals[0].should be_within(1.0).of(expected_value_per_share)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "should print successfully" do # FIXME: delete this. it's just for hacking
|
|
142
|
+
subject.print
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# shareholder_equity_statement_calculation_spec.rb
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe FinModeling::ShareholderEquityStatementCalculation do
|
|
6
|
+
before(:all) do
|
|
7
|
+
deere_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/315189/000110465910063219/0001104659-10-063219-index.htm"
|
|
8
|
+
filing = FinModeling::AnnualReportFiling.download deere_2011_annual_rpt
|
|
9
|
+
@stmt = filing.shareholder_equity_statement
|
|
10
|
+
@period = @stmt.periods.last
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe ".equity_change_calculation" do
|
|
14
|
+
subject { @stmt.equity_change_calculation }
|
|
15
|
+
it { should be_a FinModeling::EquityChangeCalculation }
|
|
16
|
+
its(:label) { should match /(stock|share)holder.*equity/i }
|
|
17
|
+
|
|
18
|
+
#let(:right_side_sum) { @stmt.liabs_and_equity_calculation.leaf_items_sum(:period=>@period) }
|
|
19
|
+
#specify { subject.leaf_items_sum(:period=>@period).should be_within(1.0).of(right_side_sum) }
|
|
20
|
+
|
|
21
|
+
it "should have the same last total as the balance sheet''s cse" do
|
|
22
|
+
pending
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe ".is_valid?" do
|
|
27
|
+
context "always... ?" do
|
|
28
|
+
it "returns true" do
|
|
29
|
+
@stmt.is_valid?.should be_true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe ".reformulated" do
|
|
35
|
+
subject { @stmt.reformulated(@period) }
|
|
36
|
+
it { should be_a FinModeling::ReformulatedShareholderEquityStatement }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe ".write_constructor" do
|
|
40
|
+
before(:all) do
|
|
41
|
+
file_name = "/tmp/finmodeling-shareholder-equity-stmt.rb"
|
|
42
|
+
item_name = "@stmt"
|
|
43
|
+
file = File.open(file_name, "w")
|
|
44
|
+
@stmt.write_constructor(file, item_name)
|
|
45
|
+
file.close
|
|
46
|
+
|
|
47
|
+
eval(File.read(file_name))
|
|
48
|
+
@loaded_stmt = eval(item_name)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context "after write_constructor()ing it to a file and then eval()ing the results" do
|
|
52
|
+
subject { @loaded_stmt }
|
|
53
|
+
it { should have_the_same_periods_as @stmt }
|
|
54
|
+
#it { should have_the_same_reformulated_last_total(:net_operating_assets).as(@stmt) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|
|
59
|
+
|
data/spec/spec_helper.rb
CHANGED
data/spec/string_helpers_spec.rb
CHANGED
|
@@ -3,21 +3,23 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
describe String do
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
let(:s) { "asdfasdf" }
|
|
7
|
+
|
|
8
|
+
describe "matches_any_regex?" do
|
|
9
|
+
context "if no regexes are provided" do
|
|
10
|
+
let(:regexes) { [] }
|
|
11
|
+
subject { s.matches_any_regex?(regexes) }
|
|
12
|
+
it { should be_false }
|
|
11
13
|
end
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
context "if the string does not match any of the regexes" do
|
|
15
|
+
let(:regexes) { [/\d/, /[A-Z]/] }
|
|
16
|
+
subject { s.matches_any_regex?(regexes) }
|
|
17
|
+
it { should be_false }
|
|
16
18
|
end
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
context "if the string matches one or more of the regexes" do
|
|
20
|
+
let(:regexes) { [/sdf/, /ddd/, /af+/] }
|
|
21
|
+
subject { s.matches_any_regex?(regexes) }
|
|
22
|
+
it { should be_true }
|
|
21
23
|
end
|
|
22
24
|
end
|
|
23
25
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe FinModeling::TimeSeriesEstimator do
|
|
4
|
+
|
|
5
|
+
describe ".new" do
|
|
6
|
+
let(:a) { 1.0 }
|
|
7
|
+
let(:b) { 0.2 }
|
|
8
|
+
subject { FinModeling::TimeSeriesEstimator.new(a, b) }
|
|
9
|
+
|
|
10
|
+
it { should be_a FinModeling::TimeSeriesEstimator }
|
|
11
|
+
its(:a) { should be_within(0.01).of(a) }
|
|
12
|
+
its(:b) { should be_within(0.01).of(b) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe ".estimate_on" do
|
|
16
|
+
let(:a) { 1.0 }
|
|
17
|
+
let(:b) { 0.2 }
|
|
18
|
+
let(:estimator) { FinModeling::TimeSeriesEstimator.new(a, b) }
|
|
19
|
+
|
|
20
|
+
context "when predicting today's outcome" do
|
|
21
|
+
let(:date) { Date.today }
|
|
22
|
+
subject { estimator.estimate_on(date) }
|
|
23
|
+
|
|
24
|
+
it { should be_a Float }
|
|
25
|
+
it { should be_within(0.01).of(a) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
context "when predicting any other day" do
|
|
29
|
+
let(:date) { Date.parse("2014-01-01") }
|
|
30
|
+
let(:num_days) { date - Date.today }
|
|
31
|
+
subject { estimator.estimate_on(date) }
|
|
32
|
+
|
|
33
|
+
it { should be_a Float }
|
|
34
|
+
it { should be_within(0.01).of(a + (b*num_days)) }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe "#from_time_series" do
|
|
39
|
+
let(:ys) { [ 10, 20 ] }
|
|
40
|
+
let(:dates) { [ (Date.today - 1), (Date.today) ] }
|
|
41
|
+
subject { FinModeling::TimeSeriesEstimator.from_time_series(dates, ys) }
|
|
42
|
+
let(:expected_a) { 20 }
|
|
43
|
+
let(:expected_b) { 20-10 }
|
|
44
|
+
|
|
45
|
+
it { should be_a FinModeling::TimeSeriesEstimator }
|
|
46
|
+
its(:a) { should be_within(0.01).of(expected_a) }
|
|
47
|
+
its(:b) { should be_within(0.01).of(expected_b) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe "#from_const" do
|
|
51
|
+
let(:y) { [ 10 ] }
|
|
52
|
+
subject { FinModeling::TimeSeriesEstimator.from_const(y) }
|
|
53
|
+
let(:expected_a) { 10 }
|
|
54
|
+
let(:expected_b) { 0 }
|
|
55
|
+
|
|
56
|
+
it { should be_a FinModeling::TimeSeriesEstimator }
|
|
57
|
+
its(:a) { should be_within(0.01).of(expected_a) }
|
|
58
|
+
its(:b) { should be_within(0.01).of(expected_b) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe FinModeling::TrailingAvgForecastingPolicy do
|
|
4
|
+
before (:all) do
|
|
5
|
+
@vals = { :revenue_estimator => FinModeling::TimeSeriesEstimator.new(0.04, 0.0),
|
|
6
|
+
:sales_pm_estimator => FinModeling::TimeSeriesEstimator.new(0.20, 0.0),
|
|
7
|
+
:fi_over_nfa_estimator => FinModeling::TimeSeriesEstimator.new(0.01, 0.0),
|
|
8
|
+
:sales_over_noa_estimator => FinModeling::TimeSeriesEstimator.new(2.00, 0.0) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
let(:policy) { FinModeling::TrailingAvgForecastingPolicy.new(@vals) }
|
|
12
|
+
let(:date) { Date.today }
|
|
13
|
+
|
|
14
|
+
describe ".revenue_on" do
|
|
15
|
+
subject { policy.revenue_on(date) }
|
|
16
|
+
it { should be_a Float }
|
|
17
|
+
it { should be_within(0.01).of(@vals[:revenue_estimator].a) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe ".sales_pm_on" do
|
|
21
|
+
subject { policy.sales_pm_on(date) }
|
|
22
|
+
it { should be_a Float }
|
|
23
|
+
it { should be_within(0.01).of(@vals[:sales_pm_estimator].a) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe ".fi_over_nfa_on" do
|
|
27
|
+
subject { policy.fi_over_nfa_on(date) }
|
|
28
|
+
it { should be_a Float }
|
|
29
|
+
it { should be_within(0.01).of(@vals[:fi_over_nfa_estimator].a) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe ".sales_over_noa_on" do
|
|
33
|
+
subject { policy.sales_over_noa_on(date) }
|
|
34
|
+
it { should be_a Float }
|
|
35
|
+
it { should be_within(0.01).of(@vals[:sales_over_noa_estimator].a) }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe FinModeling::WeightedAvgCostOfCapital do
|
|
4
|
+
let(:equity_market_val) { 2.2*1000*1000*1000 }
|
|
5
|
+
let(:debt_market_val) { 997.0*1000*1000 }
|
|
6
|
+
let(:cost_of_equity) { FinModeling::Rate.new(0.0087) }
|
|
7
|
+
let(:after_tax_cost_of_debt) { FinModeling::Rate.new(0.0031) }
|
|
8
|
+
|
|
9
|
+
describe '.new' do
|
|
10
|
+
subject { FinModeling::WeightedAvgCostOfCapital.new(equity_market_val, debt_market_val, cost_of_equity, after_tax_cost_of_debt) }
|
|
11
|
+
|
|
12
|
+
it { should be_a FinModeling::WeightedAvgCostOfCapital }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe '.rate' do
|
|
16
|
+
let(:wacc) { FinModeling::WeightedAvgCostOfCapital.new(equity_market_val, debt_market_val, cost_of_equity, after_tax_cost_of_debt) }
|
|
17
|
+
subject { wacc.rate }
|
|
18
|
+
|
|
19
|
+
let(:total_val) { equity_market_val + debt_market_val }
|
|
20
|
+
let(:e_weight) { equity_market_val / total_val }
|
|
21
|
+
let(:d_weight) { debt_market_val / total_val }
|
|
22
|
+
let(:expected_wacc) { (e_weight * cost_of_equity.value) + (d_weight * after_tax_cost_of_debt.value) }
|
|
23
|
+
its(:value) { should be_within(1.0).of(expected_wacc) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe '.summary' do
|
|
27
|
+
let(:wacc) { FinModeling::WeightedAvgCostOfCapital.new(equity_market_val, debt_market_val, cost_of_equity, after_tax_cost_of_debt) }
|
|
28
|
+
subject { wacc.summary }
|
|
29
|
+
|
|
30
|
+
it { should be_a FinModeling::CalculationSummary }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH << "."
|
|
4
|
+
|
|
5
|
+
require 'finmodeling'
|
|
6
|
+
|
|
7
|
+
def get_args
|
|
8
|
+
if ARGV.length != 1
|
|
9
|
+
puts "usage #{__FILE__} <stock symbol or report URL>"
|
|
10
|
+
exit
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
args = { :stock_symbol => nil, :filing_url => nil }
|
|
14
|
+
arg = ARGV[0]
|
|
15
|
+
if arg =~ /http/
|
|
16
|
+
args[:filing_url] = arg
|
|
17
|
+
else
|
|
18
|
+
args[:stock_symbol] = arg.downcase
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
return args
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def get_company_filing_url(stock_symbol)
|
|
25
|
+
company = FinModeling::Company.find(stock_symbol)
|
|
26
|
+
raise RuntimeError.new("couldn't find company") if company.nil?
|
|
27
|
+
raise RuntimeError.new("company has no annual reports") if company.annual_reports.length == 0
|
|
28
|
+
filing_url = company.annual_reports.last.link
|
|
29
|
+
|
|
30
|
+
return filing_url
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def get_filing(filing_url)
|
|
34
|
+
filing = FinModeling::AnnualReportFiling.download(filing_url)
|
|
35
|
+
return filing
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def print_items(filing)
|
|
39
|
+
return if !filing.has_a_shareholder_equity_statement?
|
|
40
|
+
items = filing.shareholder_equity_statement.equity_change_calculation.leaf_items
|
|
41
|
+
items.each do |item|
|
|
42
|
+
puts " { :eci_type=>:c, :item_string=>\"#{item.pretty_name}\" },"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
args = get_args
|
|
47
|
+
filing_url = args[:filing_url] || get_company_filing_url(args[:stock_symbol])
|
|
48
|
+
filing = get_filing(filing_url)
|
|
49
|
+
print_items(filing)
|