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
@@ -17,7 +17,7 @@ module FinModeling
|
|
17
17
|
def summary(args)
|
18
18
|
summary_cache_key = args[:period].to_pretty_s
|
19
19
|
summary = lookup_cached_summary(summary_cache_key)
|
20
|
-
return summary if !summary.nil?
|
20
|
+
return summary if !summary.nil? && false # FIXME: get rid of "and false"
|
21
21
|
|
22
22
|
mapping = Xbrlware::ValueMapping.new
|
23
23
|
mapping.policy[:unknown] = :flip
|
@@ -30,7 +30,7 @@ module FinModeling
|
|
30
30
|
find_and_tag_special_items(args)
|
31
31
|
|
32
32
|
summary = super(:period => args[:period], :mapping => mapping)
|
33
|
-
if !lookup_cached_classifications(BASE_FILENAME, summary.rows)
|
33
|
+
if !lookup_cached_classifications(BASE_FILENAME, summary.rows) || true # FIXME: get rid of "or true"
|
34
34
|
lookahead = [4, summary.rows.length-1].min
|
35
35
|
classify_rows(ALL_STATES, NEXT_STATES, summary.rows, FinModeling::CashChangeItem, lookahead)
|
36
36
|
save_cached_classifications(BASE_FILENAME, summary.rows)
|
@@ -45,7 +45,7 @@ module FinModeling
|
|
45
45
|
|
46
46
|
def find_and_tag_special_items(args)
|
47
47
|
leaf_items(:period => args[:period]).each do |item|
|
48
|
-
if item.name.
|
48
|
+
if item.name.matches_any_regex?([ /NetIncomeLoss/,
|
49
49
|
/ProfitLoss/ ])
|
50
50
|
item.def = {} if !item.def
|
51
51
|
item.def["xbrli:balance"] = "netincome"
|
File without changes
|
File without changes
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module FinModeling
|
2
|
+
class CashChangeSummaryFromDifferences
|
3
|
+
def initialize(re_cfs1, re_cfs2)
|
4
|
+
@re_cfs1 = re_cfs1
|
5
|
+
@re_cfs2 = re_cfs2
|
6
|
+
end
|
7
|
+
def filter_by_type(key)
|
8
|
+
case key
|
9
|
+
when :c
|
10
|
+
@cs = FinModeling::CalculationSummary.new
|
11
|
+
@cs.title = "Cash from Operations"
|
12
|
+
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @re_cfs1.cash_from_operations.total] ),
|
13
|
+
CalculationRow.new(:key => "Second Row", :vals => [-@re_cfs2.cash_from_operations.total] ) ]
|
14
|
+
return @cs
|
15
|
+
when :i
|
16
|
+
@cs = FinModeling::CalculationSummary.new
|
17
|
+
@cs.title = "Cash Investments in Operations"
|
18
|
+
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @re_cfs1.cash_investments_in_operations.total] ),
|
19
|
+
CalculationRow.new(:key => "Second Row", :vals => [-@re_cfs2.cash_investments_in_operations.total] ) ]
|
20
|
+
return @cs
|
21
|
+
when :d
|
22
|
+
@cs = FinModeling::CalculationSummary.new
|
23
|
+
@cs.title = "Payments to Debtholders"
|
24
|
+
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @re_cfs1.payments_to_debtholders.total] ),
|
25
|
+
CalculationRow.new(:key => "Second Row", :vals => [-@re_cfs2.payments_to_debtholders.total] ) ]
|
26
|
+
return @cs
|
27
|
+
when :f
|
28
|
+
@cs = FinModeling::CalculationSummary.new
|
29
|
+
@cs.title = "Payments to Stockholders"
|
30
|
+
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @re_cfs1.payments_to_stockholders.total] ),
|
31
|
+
CalculationRow.new(:key => "Second Row", :vals => [-@re_cfs2.payments_to_stockholders.total] ) ]
|
32
|
+
return @cs
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FinModeling
|
4
|
+
|
5
|
+
class CashFlowStatementAnalyses < CalculationSummary
|
6
|
+
def initialize(calc_summary)
|
7
|
+
@title = calc_summary.title
|
8
|
+
@rows = calc_summary.rows
|
9
|
+
@header_row = calc_summary.header_row
|
10
|
+
@key_width = calc_summary.key_width
|
11
|
+
@val_width = calc_summary.val_width
|
12
|
+
@max_decimals = calc_summary.max_decimals
|
13
|
+
@totals_row_enabled = false
|
14
|
+
end
|
15
|
+
|
16
|
+
def print_regressions # FIXME: rename
|
17
|
+
lr = ni_over_c_row.valid_vals.linear_regression
|
18
|
+
puts "\t\tNI / C: "+
|
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
|
+
"σ²:#{ni_over_c_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
|
+
end
|
25
|
+
|
26
|
+
def ni_over_c_row
|
27
|
+
find_row_by_key('NI / C')
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_row_by_key(key) # FIXME: move this to CalculationSummary
|
31
|
+
self.rows.find{ |x| x.key == key }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
@@ -1,28 +1,32 @@
|
|
1
1
|
module FinModeling
|
2
2
|
class CashFlowStatementCalculation < CompanyFilingCalculation
|
3
|
+
include CanChooseSuccessivePeriods
|
4
|
+
|
5
|
+
CASH_GOAL = "cash change"
|
6
|
+
CASH_LABELS = [ /^cash and cash equivalents period increase decrease/,
|
7
|
+
/^(|net )(change|increase|decrease|decrease *increase|increase *decrease) in cash and(| cash) equivalents/,
|
8
|
+
/^net cash provided by used in (|operating activities )continuing operations/,
|
9
|
+
/^net cash provided by used in operating activities/]
|
10
|
+
CASH_ANTI_LABELS = [ ]
|
11
|
+
CASH_IDS = [ /^(|Locator_|loc_)(|us-gaap_)CashAndCashEquivalentsPeriodIncreaseDecrease[_a-z0-9]+/,
|
12
|
+
/^(|Locator_|loc_)(|us-gaap_)NetCashProvidedByUsedIn(|OperatingActivities)ContinuingOperations[_a-z0-9]+/ ]
|
3
13
|
|
4
14
|
def cash_change_calculation
|
5
|
-
|
6
|
-
friendly_goal = "cash change"
|
7
|
-
label_regexes = [ /^cash and cash equivalents period increase decrease/,
|
8
|
-
/^(|net )(change|increase|decrease|decrease *increase|increase *decrease) in cash and cash equivalents/,
|
9
|
-
/^net cash provided by used in continuing operations/]
|
10
|
-
id_regexes = [ /^(|Locator_|loc_)(|us-gaap_)CashAndCashEquivalentsPeriodIncreaseDecrease[_a-z0-9]+/,
|
11
|
-
/^(|Locator_|loc_)(|us-gaap_)NetCashProvidedByUsedInContinuingOperations[_a-z0-9]+/ ]
|
12
|
-
|
13
|
-
calc = find_and_verify_calculation_arc(friendly_goal, label_regexes, id_regexes)
|
14
|
-
@cash_change = CashChangeCalculation.new(calc)
|
15
|
-
end
|
16
|
-
return @cash_change
|
15
|
+
@cash_change ||= CashChangeCalculation.new(find_calculation_arc(CASH_GOAL, CASH_LABELS, CASH_ANTI_LABELS, CASH_IDS))
|
17
16
|
end
|
18
17
|
|
19
18
|
def is_valid?
|
20
19
|
re_cfs = reformulated(periods.last)
|
21
20
|
flows_are_balanced = (re_cfs.free_cash_flow.total == (-1*re_cfs.financing_flows.total))
|
21
|
+
puts "flows are not balanced" if !flows_are_balanced
|
22
22
|
none_are_zero = (re_cfs.cash_from_operations.total != 0) &&
|
23
23
|
(re_cfs.cash_investments_in_operations.total != 0) &&
|
24
|
-
(re_cfs.payments_to_debtholders.total != 0)
|
25
|
-
(re_cfs.payments_to_stockholders.total != 0)
|
24
|
+
(re_cfs.payments_to_debtholders.total != 0) #&&
|
25
|
+
#(re_cfs.payments_to_stockholders.total != 0) # I relaxed this constraint. Seems it is often legitimately zero
|
26
|
+
puts "(re_cfs.cash_from_operations.total == 0)" if (re_cfs.cash_from_operations.total == 0)
|
27
|
+
puts "(re_cfs.cash_investments_in_operations.total == 0)" if (re_cfs.cash_investments_in_operations.total == 0)
|
28
|
+
puts "(re_cfs.payments_to_debtholders.total == 0)" if (re_cfs.payments_to_debtholders.total == 0)
|
29
|
+
#puts "(re_cfs.payments_to_stockholders.total == 0)" if (re_cfs.payments_to_stockholders.total == 0)
|
26
30
|
return (flows_are_balanced && none_are_zero)
|
27
31
|
end
|
28
32
|
|
@@ -30,46 +34,18 @@ module FinModeling
|
|
30
34
|
return ReformulatedCashFlowStatement.new(period, cash_change_calculation.summary(:period => period))
|
31
35
|
end
|
32
36
|
|
33
|
-
def latest_quarterly_reformulated(
|
34
|
-
if cash_change_calculation.periods.quarterly.any?
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
return nil
|
40
|
-
|
41
|
-
elsif cash_change_calculation.periods.halfyearly.any? &&
|
42
|
-
prev_cash_flow_statement.cash_change_calculation.periods.quarterly.any?
|
43
|
-
cfs_period = cash_change_calculation.periods.halfyearly.last
|
44
|
-
re_cfs = reformulated(cfs_period)
|
45
|
-
|
46
|
-
period_1q_thru_1q = prev_cash_flow_statement.cash_change_calculation.periods.quarterly.last
|
47
|
-
prev1q = prev_cash_flow_statement.reformulated(period_1q_thru_1q)
|
48
|
-
re_cfs = re_cfs - prev1q
|
49
|
-
|
50
|
-
return re_cfs
|
51
|
-
|
52
|
-
elsif cash_change_calculation.periods.threequarterly.any? &&
|
53
|
-
prev_cash_flow_statement.cash_change_calculation.periods.halfyearly.any?
|
54
|
-
cfs_period = cash_change_calculation.periods.threequarterly.last
|
55
|
-
re_cfs = reformulated(cfs_period)
|
56
|
-
|
57
|
-
period_1q_thru_2q = prev_cash_flow_statement.cash_change_calculation.periods.halfyearly.last
|
58
|
-
prev2q = prev_cash_flow_statement.reformulated(period_1q_thru_2q)
|
59
|
-
re_cfs = re_cfs - prev2q
|
60
|
-
|
61
|
-
return re_cfs
|
37
|
+
def latest_quarterly_reformulated(prev_cfs)
|
38
|
+
if cash_change_calculation.periods.quarterly.any?
|
39
|
+
period = cash_change_calculation.periods.quarterly.last
|
40
|
+
lqr = reformulated(period)
|
41
|
+
return lqr if lqr.flows_are_plausible?
|
42
|
+
end
|
62
43
|
|
63
|
-
|
64
|
-
prev_cash_flow_statement.cash_change_calculation.periods.threequarterly.any?
|
65
|
-
cfs_period = cash_change_calculation.periods.yearly.last
|
66
|
-
re_cfs = reformulated(cfs_period)
|
67
|
-
|
68
|
-
period_1q_thru_3q = prev_cash_flow_statement.cash_change_calculation.periods.threequarterly.last
|
69
|
-
prev3q = prev_cash_flow_statement.reformulated(period_1q_thru_3q)
|
70
|
-
re_cfs = re_cfs - prev3q
|
44
|
+
return nil if !prev_cfs
|
71
45
|
|
72
|
-
|
46
|
+
cur_period, prev_period = choose_successive_periods(cash_change_calculation, prev_cfs.cash_change_calculation)
|
47
|
+
if cur_period && prev_period
|
48
|
+
return reformulated(cur_period) - prev_cfs.reformulated(prev_period)
|
73
49
|
end
|
74
50
|
|
75
51
|
return nil
|
@@ -5,7 +5,9 @@ module FinModeling
|
|
5
5
|
FinModeling::AssetsItem.load_vectors_and_train
|
6
6
|
FinModeling::LiabsAndEquityItem.load_vectors_and_train
|
7
7
|
FinModeling::IncomeStatementItem.load_vectors_and_train
|
8
|
+
FinModeling::ComprehensiveIncomeStatementItem.load_vectors_and_train
|
8
9
|
FinModeling::CashChangeItem.load_vectors_and_train
|
10
|
+
FinModeling::EquityChangeItem.load_vectors_and_train
|
9
11
|
end
|
10
12
|
end
|
11
13
|
end
|
data/lib/finmodeling/company.rb
CHANGED
File without changes
|
@@ -1,16 +1,39 @@
|
|
1
1
|
module FinModeling
|
2
2
|
|
3
3
|
class CachedAnnualFiling
|
4
|
-
attr_accessor :balance_sheet, :income_statement, :cash_flow_statement, :disclosures
|
5
|
-
def initialize(bs, is, cfs, disclosures)
|
6
|
-
@balance_sheet
|
7
|
-
@income_statement
|
8
|
-
@
|
9
|
-
@
|
4
|
+
attr_accessor :balance_sheet, :income_statement, :comprehensive_income_statement, :cash_flow_statement, :shareholder_equity_statement, :disclosures
|
5
|
+
def initialize(bs, is, cis, cfs, ses, disclosures)
|
6
|
+
@balance_sheet = bs
|
7
|
+
@income_statement = is
|
8
|
+
@comprehensive_income_statement = cis
|
9
|
+
@cash_flow_statement = cfs
|
10
|
+
@shareholder_equity_statement = ses
|
11
|
+
@disclosures = disclosures
|
12
|
+
end
|
13
|
+
|
14
|
+
def has_an_income_statement?
|
15
|
+
!@income_statement.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def has_a_comprehensive_income_statement?
|
19
|
+
!@comprehensive_income_statement.nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
def has_a_shareholder_equity_statement?
|
23
|
+
!@shareholder_equity_statement.nil?
|
10
24
|
end
|
11
25
|
|
12
26
|
def is_valid?
|
13
|
-
|
27
|
+
puts "balance sheet is not valid" if !@balance_sheet.is_valid?
|
28
|
+
puts "income statment is not valid" if has_an_income_statement? && !@income_statement.is_valid?
|
29
|
+
puts "comprehensive income statment is not valid" if has_a_comprehensive_income_statement? && !@comprehensive_income_statement.is_valid?
|
30
|
+
#puts "cash flow statement is not valid" if !cash_flow_statement.is_valid?
|
31
|
+
|
32
|
+
return false if !@balance_sheet.is_valid?
|
33
|
+
return false if has_an_income_statement? && !@income_statement.is_valid?
|
34
|
+
return false if has_a_comprehensive_income_statement? && !@comprehensive_income_statement.is_valid?
|
35
|
+
#return false if !@cash_flow_statement.is_valid? # FIXME: why can't we enable this?
|
36
|
+
return true
|
14
37
|
end
|
15
38
|
end
|
16
39
|
|
@@ -56,15 +56,25 @@ module FinModeling
|
|
56
56
|
|
57
57
|
protected
|
58
58
|
|
59
|
-
def
|
60
|
-
|
59
|
+
def find_calculation_arc(friendly_goal, label_regexes, anti_label_regexes, id_regexes, criterion=:first)
|
60
|
+
calcs = @calculation.arcs.select{ |x| x.label.downcase.gsub(/[^a-z ]/, '').matches_any_regex?(label_regexes) &&
|
61
|
+
!x.label.downcase.gsub(/[^a-z ]/, '').matches_any_regex?(anti_label_regexes) }
|
61
62
|
|
62
|
-
if
|
63
|
-
summary_of_arcs = @calculation.arcs.map{ |x| "\"#{x.label}\"" }.join("
|
64
|
-
raise
|
63
|
+
if calcs.empty?
|
64
|
+
summary_of_arcs = @calculation.arcs.map{ |x| "\t\"#{x.label}\"" }.join("\n")
|
65
|
+
raise InvalidFilingError.new("Couldn't find #{friendly_goal} in:\n" + summary_of_arcs + "\nTried: #{label_regexes.inspect}. (Ignoring: #{anti_label_regexes.inspect}.).")
|
65
66
|
end
|
66
67
|
|
67
|
-
|
68
|
+
calc = case
|
69
|
+
when criterion == :first
|
70
|
+
calcs.first
|
71
|
+
when criterion == :longest
|
72
|
+
calcs.sort{ |x,y| x.leaf_items(periods.last).length <=> y.leaf_items(periods.last).length }.last
|
73
|
+
else
|
74
|
+
raise ArgumentError.new("\"#{criterion}\" is not a valid criterion")
|
75
|
+
end
|
76
|
+
|
77
|
+
if !calc.item_id.matches_any_regex?(id_regexes)
|
68
78
|
puts "Warning: #{friendly_goal} id is not recognized: #{calc.item_id}"
|
69
79
|
end
|
70
80
|
|
@@ -1,72 +1,138 @@
|
|
1
1
|
module FinModeling
|
2
2
|
class CompanyFilings < Array
|
3
|
+
def re_bs_arr
|
4
|
+
@re_bs_arr ||= self.map{ |filing| filing.balance_sheet.reformulated(filing.balance_sheet.periods.last) }
|
5
|
+
end
|
6
|
+
|
7
|
+
def re_is_arr
|
8
|
+
prev_stmt = nil
|
9
|
+
@re_is_arr ||= ([nil] + self).each_cons(2).map do |prev_filing, filing|
|
10
|
+
cur_re_is = nil
|
11
|
+
cur_ci_calc = filing.has_a_comprehensive_income_statement? ? filing.comprehensive_income_statement.comprehensive_income_calculation : nil
|
12
|
+
prev_ci_calc = (prev_filing && prev_filing.has_a_comprehensive_income_statement?) ?
|
13
|
+
prev_filing.comprehensive_income_statement.comprehensive_income_calculation : nil
|
14
|
+
if filing.has_an_income_statement?
|
15
|
+
cur_re_is = filing.income_statement.latest_quarterly_reformulated(cur_ci_calc, prev_stmt, prev_ci_calc)
|
16
|
+
prev_stmt = filing.income_statement
|
17
|
+
elsif filing.has_a_comprehensive_income_statement? &&
|
18
|
+
filing.comprehensive_income_statement
|
19
|
+
.comprehensive_income_calculation
|
20
|
+
.has_revenue_item?
|
21
|
+
cur_re_is = filing.comprehensive_income_statement.latest_quarterly_reformulated(prev_stmt, prev_ci_calc)
|
22
|
+
prev_stmt = filing.comprehensive_income_statement
|
23
|
+
else
|
24
|
+
raise RuntimeError.new("Can't create reformulated income statement")
|
25
|
+
end
|
26
|
+
cur_re_is
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def re_cfs_arr
|
31
|
+
@re_cfs_arr ||= ([nil] + self).each_cons(2).map do |prev_filing, filing|
|
32
|
+
filing.cash_flow_statement.latest_quarterly_reformulated(prev_filing ? prev_filing.cash_flow_statement : nil)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
3
36
|
def balance_sheet_analyses
|
4
37
|
if !@balance_sheet_analyses
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
re_bs = filing.balance_sheet.reformulated(filing.balance_sheet.periods.last)
|
9
|
-
next_analysis = re_bs.analysis(prev_re_bs)
|
10
|
-
|
11
|
-
@balance_sheet_analyses = @balance_sheet_analyses + next_analysis if @balance_sheet_analyses
|
12
|
-
@balance_sheet_analyses = next_analysis if !@balance_sheet_analyses
|
13
|
-
end
|
14
|
-
@balance_sheet_analyses = BalanceSheetAnalyses.new(@balance_sheet_analyses)
|
38
|
+
analyses = ([nil] + re_bs_arr).each_cons(2).map { |prev, cur| cur.analysis(prev) }
|
39
|
+
analyses.delete_if{ |x| x.nil? }
|
40
|
+
@balance_sheet_analyses = BalanceSheetAnalyses.new( analyses.inject(:+) )
|
15
41
|
end
|
16
42
|
return @balance_sheet_analyses
|
17
43
|
end
|
18
44
|
|
19
|
-
def income_statement_analyses
|
45
|
+
def income_statement_analyses(expected_rate_of_return)
|
20
46
|
if !@income_statement_analyses
|
21
|
-
|
22
|
-
self.
|
23
|
-
|
24
|
-
|
47
|
+
analyses = []
|
48
|
+
self.each_with_index do |filing, idx|
|
49
|
+
prev_re_bs = (idx > 0) ? re_bs_arr[idx-1] : nil
|
50
|
+
prev_re_is = (idx > 0) ? re_is_arr[idx-1] : nil
|
51
|
+
re_bs = re_bs_arr[idx]
|
52
|
+
re_is = re_is_arr[idx]
|
25
53
|
|
26
|
-
|
27
|
-
next_analysis = re_is.analysis(re_bs, prev_re_is, prev_re_bs) if re_is
|
28
|
-
|
29
|
-
@income_statement_analyses = @income_statement_analyses + next_analysis if @income_statement_analyses
|
30
|
-
@income_statement_analyses = next_analysis if !@income_statement_analyses
|
31
|
-
|
32
|
-
prev_re_bs, prev_re_is, prev_filing = [re_bs, re_is, filing]
|
54
|
+
analyses << (re_is ? re_is.analysis(re_bs, prev_re_is, prev_re_bs, expected_rate_of_return) : FinModeling::ReformulatedIncomeStatement.empty_analysis )
|
33
55
|
end
|
34
|
-
|
56
|
+
|
57
|
+
analyses.delete_if{ |x| x.nil? }
|
58
|
+
@income_statement_analyses = IncomeStatementAnalyses.new( analyses.inject(:+) )
|
35
59
|
end
|
36
60
|
return @income_statement_analyses
|
37
61
|
end
|
38
|
-
|
62
|
+
|
39
63
|
def cash_flow_statement_analyses
|
40
64
|
if !@cash_flow_statement_analyses
|
41
|
-
|
42
|
-
self.
|
43
|
-
re_is
|
44
|
-
re_cfs =
|
65
|
+
analyses = []
|
66
|
+
self.each_with_index do |filing, idx|
|
67
|
+
re_is = re_is_arr[idx]
|
68
|
+
re_cfs = re_cfs_arr[idx]
|
45
69
|
|
46
|
-
|
47
|
-
next_analysis = re_cfs.analysis(re_is) if re_cfs
|
48
|
-
|
49
|
-
@cash_flow_statement_analyses = @cash_flow_statement_analyses + next_analysis if @cash_flow_statement_analyses
|
50
|
-
@cash_flow_statement_analyses = next_analysis if !@cash_flow_statement_analyses
|
51
|
-
|
52
|
-
prev_filing, prev_re_cfs = [filing, re_cfs]
|
70
|
+
analyses << (re_cfs ? re_cfs.analysis(re_is) : FinModeling::ReformulatedCashFlowStatement.empty_analysis)
|
53
71
|
end
|
72
|
+
|
73
|
+
analyses.delete_if{ |x| x.nil? }
|
74
|
+
@cash_flow_statement_analyses = CashFlowStatementAnalyses.new( analyses.inject(:+) )
|
54
75
|
@cash_flow_statement_analyses.totals_row_enabled = false
|
55
76
|
end
|
56
77
|
return @cash_flow_statement_analyses
|
57
78
|
end
|
79
|
+
|
80
|
+
def disclosures(title_regex, period_type=nil)
|
81
|
+
ds = nil
|
82
|
+
self.each do |filing|
|
83
|
+
cur_disclosures = filing.disclosures
|
84
|
+
if ( disclosure = filing.disclosures.find{ |disc| disc.summary(:period => disc.periods.last)
|
85
|
+
.title
|
86
|
+
.gsub(/ \(.*/,'') =~ title_regex } )
|
87
|
+
|
88
|
+
period = case period_type
|
89
|
+
when nil then disclosure.periods.last
|
90
|
+
when :yearly then disclosure.periods.yearly.last
|
91
|
+
when :quarterly then disclosure.periods.quarterly.last
|
92
|
+
else raise RuntimeError.new("bogus period type")
|
93
|
+
end
|
94
|
+
|
95
|
+
if period
|
96
|
+
next_d = disclosure.summary(:period => period )
|
97
|
+
next_d.header_row = CalculationHeader.new(:key => "", :vals => [period.to_pretty_s.gsub(/.* to /,'')])
|
98
|
+
|
99
|
+
ds = ds + next_d if ds
|
100
|
+
ds = next_d if !ds
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
return ds
|
105
|
+
end
|
58
106
|
|
59
|
-
def choose_forecasting_policy
|
60
|
-
if length < 3
|
61
|
-
|
62
|
-
|
63
|
-
isa = income_statement_analyses
|
107
|
+
def choose_forecasting_policy(expected_rate_of_return, policy_type=:linear_trend)
|
108
|
+
raise RuntimeError.new("Cannot properly forecast with fewer than 3 filings") if length < 3
|
109
|
+
case policy_type
|
110
|
+
when :trailing_avg
|
111
|
+
isa = income_statement_analyses(expected_rate_of_return)
|
112
|
+
args = { }
|
113
|
+
|
114
|
+
args[:revenue_estimator] = TimeSeriesEstimator.from_const(ArrayWithStats.new(re_is_arr[1..-1].map{ |re_is| re_is.operating_revenues.total }).mean)
|
115
|
+
args[:sales_pm_estimator] = TimeSeriesEstimator.from_const(ArrayWithStats.new(isa.operating_pm_row.valid_vals[1..-1]).mean)
|
116
|
+
args[:sales_over_noa_estimator] = TimeSeriesEstimator.from_const(ArrayWithStats.new(isa.sales_over_noa_row.valid_vals[1..-1]).mean)
|
117
|
+
args[:fi_over_nfa_estimator] = TimeSeriesEstimator.from_const(ArrayWithStats.new(isa.fi_over_nfa_row.valid_vals[1..-1]).mean)
|
118
|
+
return FinModeling::LinearTrendForecastingPolicy.new(args)
|
119
|
+
|
120
|
+
when :linear_trend
|
121
|
+
isa = income_statement_analyses(expected_rate_of_return)
|
64
122
|
args = { }
|
65
|
-
|
66
|
-
args[:
|
67
|
-
|
68
|
-
args[:
|
69
|
-
|
123
|
+
|
124
|
+
args[:revenue_estimator] = TimeSeriesEstimator.from_time_series(re_is_arr[1..-1].map{ |re_is| re_is.period.value["end_date"] },
|
125
|
+
re_is_arr[1..-1].map{ |re_is| re_is.operating_revenues.total })
|
126
|
+
args[:sales_pm_estimator] = TimeSeriesEstimator.from_time_series(re_is_arr[1..-1].map{ |re_is| re_is.period.value["end_date"] },
|
127
|
+
isa.operating_pm_row.vals[1..-1])
|
128
|
+
args[:sales_over_noa_estimator] = TimeSeriesEstimator.from_time_series(re_is_arr[1..-1].map{ |re_is| re_is.period.value["end_date"] },
|
129
|
+
isa.sales_over_noa_row.vals[1..-1])
|
130
|
+
args[:fi_over_nfa_estimator] = TimeSeriesEstimator.from_time_series(re_is_arr[1..-1].map{ |re_is| re_is.period.value["end_date"] },
|
131
|
+
isa.fi_over_nfa_row.vals[1..-1])
|
132
|
+
return FinModeling::LinearTrendForecastingPolicy.new(args)
|
133
|
+
|
134
|
+
else
|
135
|
+
raise ArgumentError.new("\"#{policy_type}\" is not a valid forecasting policy type")
|
70
136
|
end
|
71
137
|
end
|
72
138
|
|
@@ -77,7 +143,7 @@ module FinModeling
|
|
77
143
|
|
78
144
|
last_last_is = (self.length >= 2) ? self[-2].income_statement : nil
|
79
145
|
puts "warning: last_last_is is nil..." if !last_last_is
|
80
|
-
last_re_is = self.last.income_statement.latest_quarterly_reformulated(last_last_is)
|
146
|
+
last_re_is = self.last.income_statement.latest_quarterly_reformulated(last_cis=nil, last_last_is, last_last_cis=nil)
|
81
147
|
raise RuntimeError.new("last_re_is is nil!") if !last_re_is
|
82
148
|
|
83
149
|
num_quarters.times do |i|
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module FinModeling
|
2
|
+
class ComprehensiveIncomeCalculation < CompanyFilingCalculation
|
3
|
+
include CanCacheClassifications
|
4
|
+
include CanCacheSummaries
|
5
|
+
include CanClassifyRows
|
6
|
+
|
7
|
+
BASE_FILENAME = File.join(FinModeling::BASE_PATH, "summaries/comprehensive_income_")
|
8
|
+
|
9
|
+
ALL_STATES = [ :or, :cogs, :oe, :oibt, :fibt, :tax, :ooiat, :fiat, :ni, :ooci, :ooci_nci, :foci, :unkoci ]
|
10
|
+
NEXT_STATES = { nil => [ :or, :ni ],
|
11
|
+
:or => [ :or, :cogs, :oe, :oibt, :fibt ],
|
12
|
+
:cogs => [ :cogs, :oe, :oibt, :fibt, :tax ],
|
13
|
+
:oe => [ :oe, :oibt, :fibt, :tax ],
|
14
|
+
:oibt => [ :oibt, :fibt, :tax ], # obit/fibt can cycle back/forth
|
15
|
+
:fibt => [ :obit, :fibt, :tax ], # obit/fibt can cycle back/forth
|
16
|
+
:tax => [ :ooiat, :fiat, :ooci, :ooci_nci, :foci, :unkoci ], # 1 tax item. then moves forward.
|
17
|
+
:ooiat => [ :ooiat, :fiat, :ooci, :ooci_nci, :foci, :unkoci ], # ooiat/fiat can cycle back/forth
|
18
|
+
:fiat => [ :ooiat, :fiat, :ooci, :ooci_nci, :foci, :unkoci ], # ooiat/fiat can cycle back/forth
|
19
|
+
|
20
|
+
:ni => [ :ooci, :ooci_nci, :foci, :unkoci ], # after ni, no ordering
|
21
|
+
|
22
|
+
:ooci => [ :ooci, :ooci_nci, :foci, :unkoci ], # after ni, no ordering
|
23
|
+
:ooci_nci => [ :ooci, :ooci_nci, :foci, :unkoci ], # after ni, no ordering
|
24
|
+
:foci => [ :ooci, :ooci_nci, :foci, :unkoci ], # after ni, no ordering
|
25
|
+
:unkoci => [ :ooci, :ooci_nci, :foci, :unkoci ] }# after ni, no ordering
|
26
|
+
|
27
|
+
def summary(args)
|
28
|
+
summary_cache_key = args[:period].to_pretty_s
|
29
|
+
thesummary = lookup_cached_summary(summary_cache_key)
|
30
|
+
return thesummary if !thesummary.nil?
|
31
|
+
|
32
|
+
mapping = Xbrlware::ValueMapping.new
|
33
|
+
mapping.policy[:debit] = :flip
|
34
|
+
|
35
|
+
thesummary = super(:period => args[:period], :mapping => mapping)
|
36
|
+
if !lookup_cached_classifications(BASE_FILENAME, thesummary.rows)
|
37
|
+
lookahead = [4, thesummary.rows.length-1].min
|
38
|
+
classify_rows(ALL_STATES, NEXT_STATES, thesummary.rows, FinModeling::ComprehensiveIncomeStatementItem, lookahead)
|
39
|
+
save_cached_classifications(BASE_FILENAME, thesummary.rows)
|
40
|
+
end
|
41
|
+
|
42
|
+
save_cached_summary(summary_cache_key, thesummary)
|
43
|
+
|
44
|
+
return thesummary
|
45
|
+
end
|
46
|
+
|
47
|
+
def has_revenue_item?
|
48
|
+
@has_revenue_item ||= summary(:period => periods.last).rows.any? do |row|
|
49
|
+
row.type == :or
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def has_net_income_item?
|
54
|
+
@has_net_income_item ||= summary(:period => periods.last).rows.any? do |row|
|
55
|
+
row.type == :ni
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module FinModeling
|
2
|
+
class ComprehensiveIncomeStatementCalculation < CompanyFilingCalculation
|
3
|
+
include CanChooseSuccessivePeriods
|
4
|
+
|
5
|
+
CI_GOAL = "comprehensive income"
|
6
|
+
CI_LABELS = [ /^comprehensive (income|loss|loss income|income loss)(| net of tax)(| attributable to .*)$/ ]
|
7
|
+
CI_ANTI_LABELS = [ /noncontrolling interest/,
|
8
|
+
/minority interest/ ]
|
9
|
+
CI_IDS = [ /^(|Locator_|loc_)(|us-gaap_)ComprehensiveIncomeNetOfTax[_0-9a-z]+/ ]
|
10
|
+
def comprehensive_income_calculation
|
11
|
+
begin
|
12
|
+
@ci ||= ComprehensiveIncomeCalculation.new(find_calculation_arc(CI_GOAL, CI_LABELS, CI_ANTI_LABELS, CI_IDS))
|
13
|
+
rescue FinModeling::InvalidFilingError => e
|
14
|
+
pre_msg = "calculation tree:\n" + self.calculation.sprint_tree
|
15
|
+
raise e, pre_msg+e.message, e.backtrace
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def is_valid?
|
20
|
+
if !comprehensive_income_calculation.has_net_income_item? && !comprehensive_income_calculation.has_revenue_item?
|
21
|
+
puts "comprehensive income statement's comprehensive income calculation lacks net income item"
|
22
|
+
puts "comprehensive income statement's comprehensive income calculation lacks sales/revenue item"
|
23
|
+
if comprehensive_income_calculation
|
24
|
+
puts "summary:"
|
25
|
+
comprehensive_income_calculation.summary(:period => periods.last).print
|
26
|
+
end
|
27
|
+
puts "calculation tree:\n" + self.calculation.sprint_tree(indent_count=0, simplified=true)
|
28
|
+
end
|
29
|
+
return (comprehensive_income_calculation.has_revenue_item? || comprehensive_income_calculation.has_net_income_item?)
|
30
|
+
end
|
31
|
+
|
32
|
+
def reformulated(period, dummy_comprehensive_income_calculation) # 2nd param is just to keep signature consistent w/ IncomeStatement::reformulated
|
33
|
+
# The way ReformulatedIncomeStatement.new() is implemented, it'll just ignore rows with types it
|
34
|
+
# doesn't know about (like OCI). So this should extract just the NI-related rows.
|
35
|
+
return ReformulatedIncomeStatement.new(period,
|
36
|
+
comprehensive_income_calculation.summary(:period=>period), # NI
|
37
|
+
comprehensive_income_calculation.summary(:period=>period)) # CI
|
38
|
+
end
|
39
|
+
|
40
|
+
def latest_quarterly_reformulated(dummy_cur_ci_calc, prev_stmt, prev_ci_calc)
|
41
|
+
if comprehensive_income_calculation.periods.quarterly.any?
|
42
|
+
period = comprehensive_income_calculation.periods.quarterly.last
|
43
|
+
lqr = reformulated(period, comprehensive_income_calculation)
|
44
|
+
|
45
|
+
if (lqr.operating_revenues.total.abs > 1.0) && # FIXME: make an is_valid here?
|
46
|
+
(lqr.cost_of_revenues .total.abs > 1.0) # FIXME: make an is_valid here?
|
47
|
+
return lqr
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
return nil if !prev_stmt
|
52
|
+
|
53
|
+
prev_calc = prev_stmt.respond_to?(:net_income_calculation) ? prev_stmt.net_income_calculation : prev_stmt.comprehensive_income_calculation
|
54
|
+
|
55
|
+
cur_period, prev_period = choose_successive_periods(comprehensive_income_calculation, prev_calc)
|
56
|
+
if cur_period && prev_period
|
57
|
+
new_re_is = reformulated(cur_period, comprehensive_income_calculation) - prev_stmt.reformulated(prev_period, prev_ci_calc)
|
58
|
+
# the above subtraction doesn't know what period you want. So let's patch the result to have
|
59
|
+
# a quarterly period with the right end-points
|
60
|
+
new_re_is.period = Xbrlware::Context::Period.new({"start_date"=>prev_period.value["end_date"],
|
61
|
+
"end_date" =>cur_period.value["end_date"]})
|
62
|
+
return new_re_is
|
63
|
+
end
|
64
|
+
|
65
|
+
return nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def write_constructor(file, item_name)
|
69
|
+
item_calc_name = item_name + "_calc"
|
70
|
+
@calculation.write_constructor(file, item_calc_name)
|
71
|
+
file.puts "#{item_name} = FinModeling::ComprehensiveIncomeStatementCalculation.new(#{item_calc_name})"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module FinModeling
|
2
|
+
class ComprehensiveIncomeStatementItem < String
|
3
|
+
include HasStringClassifier
|
4
|
+
|
5
|
+
BASE_FILENAME = File.join(FinModeling::BASE_PATH, "classifiers/cisi_")
|
6
|
+
TYPES = [ :or, :cogs, :oe, :oibt, :fibt, :tax, :ooiat, :fiat, :ni, :ooci, :ooci_nci, :foci, :unkoci ]
|
7
|
+
# same as in IncomeStatementItem, plus four new types:
|
8
|
+
# ni (net income -- optional, for when it is rolled up, versus (more typically) presented in the same detail as in the income statement)
|
9
|
+
# ooci (operating other comperhensive income)
|
10
|
+
# ooci_nci (operating other comperhensive income - non-controling interest)
|
11
|
+
# foci (financial other comperhensive income)
|
12
|
+
# unkoci (unknown (either operating or financial) other comperhensive income)
|
13
|
+
|
14
|
+
has_string_classifier(TYPES, ComprehensiveIncomeStatementItem)
|
15
|
+
|
16
|
+
def self.load_vectors_and_train
|
17
|
+
self._load_vectors_and_train(BASE_FILENAME, FinModeling::ComprehensiveIncomeStatementItem::TRAINING_VECTORS)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|