finmodeling 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. data/.gitignore +0 -0
  2. data/Gemfile +2 -0
  3. data/README.md +289 -269
  4. data/Rakefile +12 -0
  5. data/TODO.txt +113 -20
  6. data/examples/{dump_report.rb → dump_latest_10k.rb} +1 -1
  7. data/examples/list_disclosures.rb +50 -0
  8. data/examples/lists/nasdaq-mid-to-mega-tech-symbols.txt +0 -0
  9. data/examples/show_report.rb +112 -32
  10. data/examples/show_reports.rb +162 -33
  11. data/finmodeling.gemspec +4 -1
  12. data/lib/finmodeling/annual_report_filing.rb +97 -18
  13. data/lib/finmodeling/array_with_stats.rb +0 -0
  14. data/lib/finmodeling/assets_calculation.rb +12 -3
  15. data/lib/finmodeling/assets_item.rb +0 -0
  16. data/lib/finmodeling/assets_item_vectors.rb +0 -0
  17. data/lib/finmodeling/balance_sheet_analyses.rb +19 -4
  18. data/lib/finmodeling/balance_sheet_calculation.rb +52 -37
  19. data/lib/finmodeling/calculation_summary.rb +119 -14
  20. data/lib/finmodeling/can_cache_classifications.rb +0 -0
  21. data/lib/finmodeling/can_cache_summaries.rb +0 -0
  22. data/lib/finmodeling/can_choose_successive_periods.rb +15 -0
  23. data/lib/finmodeling/can_classify_rows.rb +0 -0
  24. data/lib/finmodeling/capm.rb +80 -0
  25. data/lib/finmodeling/cash_change_calculation.rb +3 -3
  26. data/lib/finmodeling/cash_change_item.rb +0 -0
  27. data/lib/finmodeling/cash_change_item_vectors.rb +0 -0
  28. data/lib/finmodeling/cash_change_summary_from_differences.rb +36 -0
  29. data/lib/finmodeling/cash_flow_statement_analyses.rb +36 -0
  30. data/lib/finmodeling/cash_flow_statement_calculation.rb +28 -52
  31. data/lib/finmodeling/classifiers.rb +2 -0
  32. data/lib/finmodeling/company.rb +0 -0
  33. data/lib/finmodeling/company_filing.rb +30 -7
  34. data/lib/finmodeling/company_filing_calculation.rb +16 -6
  35. data/lib/finmodeling/company_filings.rb +112 -46
  36. data/lib/finmodeling/comprehensive_income_calculation.rb +60 -0
  37. data/lib/finmodeling/comprehensive_income_statement_calculation.rb +74 -0
  38. data/lib/finmodeling/comprehensive_income_statement_item.rb +20 -0
  39. data/lib/finmodeling/comprehensive_income_statement_item_vectors.rb +235 -0
  40. data/lib/finmodeling/config.rb +0 -0
  41. data/lib/finmodeling/debt_cost_of_capital.rb +14 -0
  42. data/lib/finmodeling/equity_change_calculation.rb +43 -0
  43. data/lib/finmodeling/equity_change_item.rb +25 -0
  44. data/lib/finmodeling/equity_change_item_vectors.rb +156 -0
  45. data/lib/finmodeling/factory.rb +0 -0
  46. data/lib/finmodeling/fama_french_cost_of_equity.rb +119 -0
  47. data/lib/finmodeling/float_helpers.rb +14 -8
  48. data/lib/finmodeling/forecasted_reformulated_balance_sheet.rb +55 -0
  49. data/lib/finmodeling/forecasted_reformulated_income_statement.rb +110 -0
  50. data/lib/finmodeling/forecasts.rb +4 -4
  51. data/lib/finmodeling/has_string_classifer.rb +0 -0
  52. data/lib/finmodeling/income_statement_analyses.rb +23 -17
  53. data/lib/finmodeling/income_statement_calculation.rb +46 -43
  54. data/lib/finmodeling/income_statement_item.rb +1 -1
  55. data/lib/finmodeling/income_statement_item_vectors.rb +24 -13
  56. data/lib/finmodeling/invalid_filing_error.rb +4 -0
  57. data/lib/finmodeling/liabs_and_equity_calculation.rb +18 -8
  58. data/lib/finmodeling/liabs_and_equity_item.rb +1 -1
  59. data/lib/finmodeling/liabs_and_equity_item_vectors.rb +24 -24
  60. data/lib/finmodeling/linear_trend_forecasting_policy.rb +23 -0
  61. data/lib/finmodeling/net_income_calculation.rb +23 -10
  62. data/lib/finmodeling/net_income_summary_from_differences.rb +51 -0
  63. data/lib/finmodeling/paths.rb +0 -0
  64. data/lib/finmodeling/period_array.rb +8 -4
  65. data/lib/finmodeling/quarterly_report_filing.rb +9 -4
  66. data/lib/finmodeling/rate.rb +8 -0
  67. data/lib/finmodeling/ratio.rb +0 -0
  68. data/lib/finmodeling/reformulated_balance_sheet.rb +47 -88
  69. data/lib/finmodeling/reformulated_cash_flow_statement.rb +18 -41
  70. data/lib/finmodeling/reformulated_income_statement.rb +44 -206
  71. data/lib/finmodeling/reformulated_shareholder_equity_statement.rb +50 -0
  72. data/lib/finmodeling/reoi_valuation.rb +104 -0
  73. data/lib/finmodeling/shareholder_equity_statement_calculation.rb +34 -0
  74. data/lib/finmodeling/string_helpers.rb +18 -1
  75. data/lib/finmodeling/time_series_estimator.rb +25 -0
  76. data/lib/finmodeling/trailing_avg_forecasting_policy.rb +23 -0
  77. data/lib/finmodeling/version.rb +1 -1
  78. data/lib/finmodeling/weighted_avg_cost_of_capital.rb +35 -0
  79. data/lib/finmodeling/yahoo_finance_helpers.rb +20 -0
  80. data/lib/finmodeling.rb +33 -2
  81. data/spec/annual_report_filing_spec.rb +81 -45
  82. data/spec/assets_calculation_spec.rb +7 -4
  83. data/spec/assets_item_spec.rb +9 -14
  84. data/spec/balance_sheet_analyses_spec.rb +13 -13
  85. data/spec/balance_sheet_calculation_spec.rb +45 -51
  86. data/spec/calculation_summary_spec.rb +113 -21
  87. data/spec/can_classify_rows_spec.rb +0 -0
  88. data/spec/cash_change_calculation_spec.rb +1 -10
  89. data/spec/cash_change_item_spec.rb +10 -18
  90. data/spec/cash_flow_statement_calculation_spec.rb +10 -24
  91. data/spec/company_beta_spec.rb +53 -0
  92. data/spec/company_filing_calculation_spec.rb +39 -49
  93. data/spec/company_filing_spec.rb +0 -0
  94. data/spec/company_filings_spec.rb +75 -25
  95. data/spec/company_spec.rb +37 -47
  96. data/spec/comprehensive_income_statement_calculation_spec.rb +54 -0
  97. data/spec/comprehensive_income_statement_item_spec.rb +56 -0
  98. data/spec/debt_cost_of_capital_spec.rb +19 -0
  99. data/spec/equity_change_calculation_spec.rb +33 -0
  100. data/spec/equity_change_item_spec.rb +58 -0
  101. data/spec/factory_spec.rb +2 -2
  102. data/spec/forecasts_spec.rb +2 -2
  103. data/spec/income_statement_analyses_spec.rb +23 -21
  104. data/spec/income_statement_calculation_spec.rb +17 -49
  105. data/spec/income_statement_item_spec.rb +17 -29
  106. data/spec/liabs_and_equity_calculation_spec.rb +6 -3
  107. data/spec/liabs_and_equity_item_spec.rb +14 -22
  108. data/spec/linear_trend_forecasting_policy_spec.rb +37 -0
  109. data/spec/matchers/custom_matchers.rb +79 -0
  110. data/spec/mocks/calculation.rb +0 -0
  111. data/spec/mocks/income_statement_analyses.rb +0 -0
  112. data/spec/mocks/sec_query.rb +0 -0
  113. data/spec/net_income_calculation_spec.rb +16 -10
  114. data/spec/period_array.rb +0 -0
  115. data/spec/quarterly_report_filing_spec.rb +21 -38
  116. data/spec/rate_spec.rb +0 -0
  117. data/spec/ratio_spec.rb +0 -0
  118. data/spec/reformulated_balance_sheet_spec.rb +56 -33
  119. data/spec/reformulated_cash_flow_statement_spec.rb +18 -10
  120. data/spec/reformulated_income_statement_spec.rb +16 -15
  121. data/spec/reformulated_shareholder_equity_statement_spec.rb +43 -0
  122. data/spec/reoi_valuation_spec.rb +146 -0
  123. data/spec/shareholder_equity_statement_calculation_spec.rb +59 -0
  124. data/spec/spec_helper.rb +4 -1
  125. data/spec/string_helpers_spec.rb +15 -13
  126. data/spec/time_series_estimator_spec.rb +61 -0
  127. data/spec/trailing_avg_forecasting_policy_spec.rb +37 -0
  128. data/spec/weighted_avg_cost_of_capital_spec.rb +32 -0
  129. data/tools/create_equity_change_training_vectors.rb +49 -0
  130. data/tools/time_specs.sh +7 -0
  131. metadata +182 -36
  132. data/lib/finmodeling/constant_forecasting_policy.rb +0 -23
  133. data/lib/finmodeling/generic_forecasting_policy.rb +0 -19
  134. data/spec/constant_forecasting_policy_spec.rb +0 -37
  135. 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? and false # FIXME: get rid of "and false"
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) or true # FIXME: get rid of "or true"
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.matches_regexes?([ /NetIncomeLoss/,
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
- if @cash_change.nil?
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(prev_cash_flow_statement)
34
- if cash_change_calculation.periods.quarterly.any? &&
35
- reformulated(cash_change_calculation.periods.quarterly.last).cash_investments_in_operations.total.abs > 1.0
36
- return reformulated(cash_change_calculation.periods.quarterly.last)
37
-
38
- elsif !prev_cash_flow_statement
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
- elsif cash_change_calculation.periods.yearly.any? &&
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
- return re_cfs
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
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 = bs
7
- @income_statement = is
8
- @cash_flow_statement = cfs
9
- @disclosures = disclosures
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
- return (@income_statement.is_valid? and @balance_sheet.is_valid? and @cash_flow_statement.is_valid?)
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 find_and_verify_calculation_arc(friendly_goal, label_regexes, id_regexes)
60
- calc = @calculation.arcs.find{ |x| x.label.downcase.gsub(/[^a-z ]/, '').matches_regexes?(label_regexes) }
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 calc.nil?
63
- summary_of_arcs = @calculation.arcs.map{ |x| "\"#{x.label}\"" }.join("; ")
64
- raise RuntimeError.new("Couldn't find #{friendly_goal} in: " + summary_of_arcs)
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
- if !calc.item_id.matches_regexes?(id_regexes)
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
- re_bs = nil
6
- self.each do |filing|
7
- prev_re_bs = re_bs
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
- prev_re_bs, prev_re_is, prev_filing = [nil, nil, nil]
22
- self.each do |filing|
23
- re_is = filing.income_statement.latest_quarterly_reformulated(prev_filing ? prev_filing.income_statement : nil)
24
- re_bs = filing.balance_sheet.reformulated(filing.balance_sheet.periods.last)
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
- next_analysis = FinModeling::ReformulatedIncomeStatement.empty_analysis if !re_is
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
- @income_statement_analyses = IncomeStatementAnalyses.new(@income_statement_analyses)
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
- prev_filing, prev_re_cfs = [nil, nil]
42
- self.each do |filing|
43
- re_is = filing.income_statement.latest_quarterly_reformulated(prev_filing ? prev_filing.income_statement : nil)
44
- re_cfs = filing.cash_flow_statement.latest_quarterly_reformulated(prev_filing ? prev_filing.cash_flow_statement : nil)
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
- next_analysis = FinModeling::ReformulatedCashFlowStatement.empty_analysis if !re_cfs
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
- return FinModeling::GenericForecastingPolicy.new
62
- else
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
- args[:revenue_growth] = isa.revenue_growth_row.valid_vals.mean
66
- args[:sales_pm ] = isa.operating_pm_row.valid_vals.mean
67
- args[:sales_over_noa] = isa.sales_over_noa_row.valid_vals.mean
68
- args[:fi_over_nfa ] = isa.fi_over_nfa_row.valid_vals.mean
69
- return FinModeling::ConstantForecastingPolicy.new(args)
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