finmodeling 0.1 → 0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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