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
@@ -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)
|