finmodeling 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/Gemfile +10 -0
- data/README.md +292 -0
- data/Rakefile +6 -0
- data/TODO.txt +36 -0
- data/examples/dump_report.rb +33 -0
- data/examples/lists/nasdaq-mid-to-mega-tech-symbols.txt +226 -0
- data/examples/show_report.rb +218 -0
- data/examples/show_reports.rb +77 -0
- data/finmodeling.gemspec +31 -0
- data/lib/finmodeling/annual_report_filing.rb +104 -0
- data/lib/finmodeling/array_with_stats.rb +22 -0
- data/lib/finmodeling/assets_calculation.rb +36 -0
- data/lib/finmodeling/assets_item.rb +14 -0
- data/lib/finmodeling/assets_item_vectors.rb +638 -0
- data/lib/finmodeling/balance_sheet_analyses.rb +33 -0
- data/lib/finmodeling/balance_sheet_calculation.rb +68 -0
- data/lib/finmodeling/calculation_summary.rb +148 -0
- data/lib/finmodeling/can_cache_classifications.rb +36 -0
- data/lib/finmodeling/can_cache_summaries.rb +16 -0
- data/lib/finmodeling/can_classify_rows.rb +54 -0
- data/lib/finmodeling/cash_change_calculation.rb +67 -0
- data/lib/finmodeling/cash_change_item.rb +14 -0
- data/lib/finmodeling/cash_change_item_vectors.rb +241 -0
- data/lib/finmodeling/cash_flow_statement_calculation.rb +85 -0
- data/lib/finmodeling/classifiers.rb +11 -0
- data/lib/finmodeling/company.rb +102 -0
- data/lib/finmodeling/company_filing.rb +64 -0
- data/lib/finmodeling/company_filing_calculation.rb +75 -0
- data/lib/finmodeling/company_filings.rb +100 -0
- data/lib/finmodeling/config.rb +37 -0
- data/lib/finmodeling/constant_forecasting_policy.rb +23 -0
- data/lib/finmodeling/factory.rb +27 -0
- data/lib/finmodeling/float_helpers.rb +17 -0
- data/lib/finmodeling/forecasts.rb +48 -0
- data/lib/finmodeling/generic_forecasting_policy.rb +19 -0
- data/lib/finmodeling/has_string_classifer.rb +96 -0
- data/lib/finmodeling/income_statement_analyses.rb +74 -0
- data/lib/finmodeling/income_statement_calculation.rb +71 -0
- data/lib/finmodeling/income_statement_item.rb +14 -0
- data/lib/finmodeling/income_statement_item_vectors.rb +654 -0
- data/lib/finmodeling/liabs_and_equity_calculation.rb +36 -0
- data/lib/finmodeling/liabs_and_equity_item.rb +14 -0
- data/lib/finmodeling/liabs_and_equity_item_vectors.rb +1936 -0
- data/lib/finmodeling/net_income_calculation.rb +41 -0
- data/lib/finmodeling/paths.rb +5 -0
- data/lib/finmodeling/period_array.rb +24 -0
- data/lib/finmodeling/quarterly_report_filing.rb +23 -0
- data/lib/finmodeling/rate.rb +20 -0
- data/lib/finmodeling/ratio.rb +20 -0
- data/lib/finmodeling/reformulated_balance_sheet.rb +176 -0
- data/lib/finmodeling/reformulated_cash_flow_statement.rb +140 -0
- data/lib/finmodeling/reformulated_income_statement.rb +436 -0
- data/lib/finmodeling/string_helpers.rb +26 -0
- data/lib/finmodeling/version.rb +3 -0
- data/lib/finmodeling.rb +70 -0
- data/spec/annual_report_filing_spec.rb +68 -0
- data/spec/assets_calculation_spec.rb +21 -0
- data/spec/assets_item_spec.rb +66 -0
- data/spec/balance_sheet_analyses_spec.rb +43 -0
- data/spec/balance_sheet_calculation_spec.rb +91 -0
- data/spec/calculation_summary_spec.rb +63 -0
- data/spec/can_classify_rows_spec.rb +86 -0
- data/spec/cash_change_calculation_spec.rb +56 -0
- data/spec/cash_change_item_spec.rb +66 -0
- data/spec/cash_flow_statement_calculation_spec.rb +108 -0
- data/spec/company_filing_calculation_spec.rb +74 -0
- data/spec/company_filing_spec.rb +30 -0
- data/spec/company_filings_spec.rb +55 -0
- data/spec/company_spec.rb +73 -0
- data/spec/constant_forecasting_policy_spec.rb +37 -0
- data/spec/factory_spec.rb +18 -0
- data/spec/forecasts_spec.rb +21 -0
- data/spec/generic_forecasting_policy_spec.rb +33 -0
- data/spec/income_statement_analyses_spec.rb +63 -0
- data/spec/income_statement_calculation_spec.rb +88 -0
- data/spec/income_statement_item_spec.rb +86 -0
- data/spec/liabs_and_equity_calculation_spec.rb +20 -0
- data/spec/liabs_and_equity_item_spec.rb +66 -0
- data/spec/mocks/calculation.rb +10 -0
- data/spec/mocks/income_statement_analyses.rb +93 -0
- data/spec/mocks/sec_query.rb +31 -0
- data/spec/net_income_calculation_spec.rb +23 -0
- data/spec/period_array.rb +52 -0
- data/spec/quarterly_report_filing_spec.rb +69 -0
- data/spec/rate_spec.rb +33 -0
- data/spec/ratio_spec.rb +33 -0
- data/spec/reformulated_balance_sheet_spec.rb +146 -0
- data/spec/reformulated_cash_flow_statement_spec.rb +174 -0
- data/spec/reformulated_income_statement_spec.rb +293 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/string_helpers_spec.rb +23 -0
- data/tools/create_balance_sheet_training_vectors.rb +65 -0
- data/tools/create_cash_change_training_vectors.rb +48 -0
- data/tools/create_credit_debit_training_vectors.rb +51 -0
- data/tools/create_income_statement_training_vectors.rb +48 -0
- metadata +289 -0
@@ -0,0 +1,436 @@
|
|
1
|
+
module FinModeling
|
2
|
+
class ReformulatedIncomeStatement
|
3
|
+
attr_accessor :period
|
4
|
+
|
5
|
+
class FakeNetIncomeSummary
|
6
|
+
def initialize(ris1, ris2)
|
7
|
+
@ris1 = ris1
|
8
|
+
@ris2 = ris2
|
9
|
+
end
|
10
|
+
def filter_by_type(key)
|
11
|
+
case key
|
12
|
+
when :or
|
13
|
+
@cs = FinModeling::CalculationSummary.new
|
14
|
+
@cs.title = "Operating Revenues"
|
15
|
+
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.operating_revenues.total] ),
|
16
|
+
CalculationRow.new(:key => "Second Row", :vals => [-@ris2.operating_revenues.total] ) ]
|
17
|
+
return @cs
|
18
|
+
when :cogs
|
19
|
+
@cs = FinModeling::CalculationSummary.new
|
20
|
+
@cs.title = "Cost of Revenues"
|
21
|
+
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.cost_of_revenues.total] ),
|
22
|
+
CalculationRow.new(:key => "Second Row", :vals => [-@ris2.cost_of_revenues.total] ) ]
|
23
|
+
return @cs
|
24
|
+
when :oe
|
25
|
+
@cs = FinModeling::CalculationSummary.new
|
26
|
+
@cs.title = "Operating Expenses"
|
27
|
+
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.operating_expenses.total] ),
|
28
|
+
CalculationRow.new(:key => "Second Row", :vals => [-@ris2.operating_expenses.total] ) ]
|
29
|
+
return @cs
|
30
|
+
when :oibt
|
31
|
+
@cs = FinModeling::CalculationSummary.new
|
32
|
+
@cs.title = "Operating Income from Sales, Before taxes"
|
33
|
+
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.operating_income_after_tax.rows[1].vals.first] ),
|
34
|
+
CalculationRow.new(:key => "Second Row", :vals => [-@ris2.operating_income_after_tax.rows[1].vals.first] ) ]
|
35
|
+
return @cs
|
36
|
+
when :fibt
|
37
|
+
@cs = FinModeling::CalculationSummary.new
|
38
|
+
@cs.title = "Financing Income, Before Taxes"
|
39
|
+
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.net_financing_income.rows[0].vals.first] ),
|
40
|
+
CalculationRow.new(:key => "Second Row", :vals => [-@ris2.net_financing_income.rows[0].vals.first] ) ]
|
41
|
+
return @cs
|
42
|
+
when :tax
|
43
|
+
@cs = FinModeling::CalculationSummary.new
|
44
|
+
@cs.title = "Taxes"
|
45
|
+
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.income_from_sales_after_tax.rows[1].vals.first] ),
|
46
|
+
CalculationRow.new(:key => "Second Row", :vals => [-@ris2.income_from_sales_after_tax.rows[1].vals.first] ) ]
|
47
|
+
return @cs
|
48
|
+
when :ooiat
|
49
|
+
@cs = FinModeling::CalculationSummary.new
|
50
|
+
@cs.title = "Other Operating Income, After Taxes"
|
51
|
+
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.operating_income_after_tax.rows[3].vals.first] ),
|
52
|
+
CalculationRow.new(:key => "Second Row", :vals => [-@ris2.operating_income_after_tax.rows[3].vals.first] ) ]
|
53
|
+
return @cs
|
54
|
+
when :fiat
|
55
|
+
@cs = FinModeling::CalculationSummary.new
|
56
|
+
@cs.title = "Financing Income, After Taxes"
|
57
|
+
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.net_financing_income.rows[2].vals.first] ),
|
58
|
+
CalculationRow.new(:key => "Second Row", :vals => [-@ris2.net_financing_income.rows[2].vals.first] ) ]
|
59
|
+
return @cs
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def initialize(period, net_income_summary, tax_rate=0.35)
|
65
|
+
@period = period
|
66
|
+
@tax_rate = tax_rate
|
67
|
+
|
68
|
+
@orev = net_income_summary.filter_by_type(:or )
|
69
|
+
@cogs = net_income_summary.filter_by_type(:cogs )
|
70
|
+
@oe = net_income_summary.filter_by_type(:oe )
|
71
|
+
@oibt = net_income_summary.filter_by_type(:oibt )
|
72
|
+
@fibt = net_income_summary.filter_by_type(:fibt )
|
73
|
+
@tax = net_income_summary.filter_by_type(:tax )
|
74
|
+
@ooiat = net_income_summary.filter_by_type(:ooiat)
|
75
|
+
@fiat = net_income_summary.filter_by_type(:fiat )
|
76
|
+
|
77
|
+
@fibt_tax_effect = (@fibt.total * @tax_rate).round.to_f
|
78
|
+
@nfi = @fibt.total + -@fibt_tax_effect + @fiat.total
|
79
|
+
|
80
|
+
@oibt_tax_effect = (@oibt.total * @tax_rate).round.to_f
|
81
|
+
|
82
|
+
@gm = @orev.total + @cogs.total
|
83
|
+
@oisbt = @gm + @oe.total
|
84
|
+
|
85
|
+
@oisat = @oisbt + @tax.total + @fibt_tax_effect + @oibt_tax_effect
|
86
|
+
|
87
|
+
@oi = @oisat + @oibt.total - @oibt_tax_effect + @ooiat.total
|
88
|
+
|
89
|
+
@ci = @nfi + @oi
|
90
|
+
end
|
91
|
+
|
92
|
+
def -(ris2)
|
93
|
+
net_income_summary = FakeNetIncomeSummary.new(self, ris2)
|
94
|
+
return ReformulatedIncomeStatement.new(@period, net_income_summary, @tax_rate)
|
95
|
+
end
|
96
|
+
|
97
|
+
def operating_revenues
|
98
|
+
@orev
|
99
|
+
end
|
100
|
+
|
101
|
+
def cost_of_revenues
|
102
|
+
@cogs
|
103
|
+
end
|
104
|
+
|
105
|
+
def gross_revenue
|
106
|
+
cs = FinModeling::CalculationSummary.new
|
107
|
+
cs.title = "Gross Revenue"
|
108
|
+
cs.rows = [ CalculationRow.new(:key => "Operating Revenues (OR)", :vals => [@orev.total] ),
|
109
|
+
CalculationRow.new(:key => "Cost of Goods Sold (COGS)", :vals => [@cogs.total] ) ]
|
110
|
+
return cs
|
111
|
+
end
|
112
|
+
|
113
|
+
def operating_expenses
|
114
|
+
@oe
|
115
|
+
end
|
116
|
+
|
117
|
+
def income_from_sales_before_tax
|
118
|
+
cs = FinModeling::CalculationSummary.new
|
119
|
+
cs.title = "Operating Income from sales, before tax (OISBT)"
|
120
|
+
cs.rows = [ CalculationRow.new(:key => "Gross Margin (GM)", :vals => [@gm] ),
|
121
|
+
CalculationRow.new(:key => "Operating Expense (OE)", :vals => [@oe.total] ) ]
|
122
|
+
return cs
|
123
|
+
end
|
124
|
+
|
125
|
+
def income_from_sales_after_tax
|
126
|
+
cs = FinModeling::CalculationSummary.new
|
127
|
+
cs.title = "Operating Income from sales, after tax (OISAT)"
|
128
|
+
cs.rows = [ CalculationRow.new(:key => "Operating income from sales (before tax)", :vals => [@oisbt] ),
|
129
|
+
CalculationRow.new(:key => "Reported taxes", :vals => [@tax.total] ),
|
130
|
+
CalculationRow.new(:key => "Taxes on net financing income", :vals => [@fibt_tax_effect] ),
|
131
|
+
CalculationRow.new(:key => "Taxes on other operating income", :vals => [@oibt_tax_effect] ) ]
|
132
|
+
return cs
|
133
|
+
end
|
134
|
+
|
135
|
+
def operating_income_after_tax
|
136
|
+
cs = FinModeling::CalculationSummary.new
|
137
|
+
cs.title = "Operating income, after tax (OI)"
|
138
|
+
cs.rows = [ CalculationRow.new(:key => "Operating income after sales, after tax (OISAT)", :vals => [@oisat] ),
|
139
|
+
CalculationRow.new(:key => "Other operating income, before tax (OIBT)", :vals => [@oibt.total] ),
|
140
|
+
CalculationRow.new(:key => "Tax on other operating income", :vals => [-@oibt_tax_effect] ),
|
141
|
+
CalculationRow.new(:key => "Other operating income, after tax (OOIAT)", :vals => [@ooiat.total] ) ]
|
142
|
+
return cs
|
143
|
+
end
|
144
|
+
|
145
|
+
def net_financing_income
|
146
|
+
cs = FinModeling::CalculationSummary.new
|
147
|
+
cs.title = "Net financing income, after tax (NFI)"
|
148
|
+
cs.rows = [ CalculationRow.new(:key => "Financing income, before tax (FIBT)", :vals => [@fibt.total] ),
|
149
|
+
CalculationRow.new(:key => "Tax effect (FIBT_TAX_EFFECT)", :vals => [-@fibt_tax_effect] ),
|
150
|
+
CalculationRow.new(:key => "Financing income, after tax (FIAT)", :vals => [@fiat.total] ) ]
|
151
|
+
return cs
|
152
|
+
end
|
153
|
+
|
154
|
+
def comprehensive_income
|
155
|
+
cs = FinModeling::CalculationSummary.new
|
156
|
+
cs.title = "Comprehensive income (CI)"
|
157
|
+
cs.rows = [ CalculationRow.new(:key => "Operating income, after tax (OI)", :vals => [@oi] ),
|
158
|
+
CalculationRow.new(:key => "Net financing income, after tax (NFI)", :vals => [@nfi] ) ]
|
159
|
+
return cs
|
160
|
+
end
|
161
|
+
|
162
|
+
def gross_margin
|
163
|
+
gross_revenue.total / operating_revenues.total
|
164
|
+
end
|
165
|
+
|
166
|
+
def sales_profit_margin
|
167
|
+
income_from_sales_after_tax.total / operating_revenues.total
|
168
|
+
end
|
169
|
+
|
170
|
+
def operating_profit_margin
|
171
|
+
operating_income_after_tax.total / operating_revenues.total
|
172
|
+
end
|
173
|
+
|
174
|
+
def fi_over_sales
|
175
|
+
net_financing_income.total / operating_revenues.total
|
176
|
+
end
|
177
|
+
|
178
|
+
def ni_over_sales
|
179
|
+
comprehensive_income.total / operating_revenues.total
|
180
|
+
end
|
181
|
+
|
182
|
+
def sales_over_noa(reformed_bal_sheet)
|
183
|
+
ratio = operating_revenues.total / reformed_bal_sheet.net_operating_assets.total
|
184
|
+
Ratio.new(ratio).annualize(from_days=@period.days, to_days=365.0)
|
185
|
+
end
|
186
|
+
|
187
|
+
def fi_over_nfa(reformed_bal_sheet)
|
188
|
+
ratio = net_financing_income.total / reformed_bal_sheet.net_financial_assets.total
|
189
|
+
Ratio.new(ratio).annualize(from_days=@period.days, to_days=365.0)
|
190
|
+
end
|
191
|
+
|
192
|
+
def revenue_growth(prev)
|
193
|
+
rate = (operating_revenues.total - prev.operating_revenues.total) / prev.operating_revenues.total
|
194
|
+
return annualize_rate(prev, rate)
|
195
|
+
end
|
196
|
+
|
197
|
+
def core_oi_growth(prev)
|
198
|
+
rate = (income_from_sales_after_tax.total - prev.income_from_sales_after_tax.total) / prev.income_from_sales_after_tax.total
|
199
|
+
return annualize_rate(prev, rate)
|
200
|
+
end
|
201
|
+
|
202
|
+
def oi_growth(prev)
|
203
|
+
rate = (operating_income_after_tax.total - prev.operating_income_after_tax.total) / prev.operating_income_after_tax.total
|
204
|
+
return annualize_rate(prev, rate)
|
205
|
+
end
|
206
|
+
|
207
|
+
def re_oi(prev_bal_sheet, expected_rate_of_return=0.10)
|
208
|
+
e_ror = deannualize_rate(prev_bal_sheet, expected_rate_of_return)
|
209
|
+
return (operating_income_after_tax.total - (e_ror * prev_bal_sheet.net_operating_assets.total))
|
210
|
+
end
|
211
|
+
|
212
|
+
def self.empty_analysis
|
213
|
+
analysis = CalculationSummary.new
|
214
|
+
analysis.title = ""
|
215
|
+
analysis.rows = []
|
216
|
+
|
217
|
+
analysis.header_row = CalculationHeader.new(:key => "", :vals => ["Unknown..."])
|
218
|
+
|
219
|
+
analysis.rows << CalculationRow.new(:key => "Revenue ($MM)", :vals => [nil])
|
220
|
+
if Config.income_detail_enabled?
|
221
|
+
analysis.rows << CalculationRow.new(:key => "COGS ($MM)", :vals => [nil])
|
222
|
+
analysis.rows << CalculationRow.new(:key => "GM ($MM)", :vals => [nil])
|
223
|
+
analysis.rows << CalculationRow.new(:key => "OE ($MM)", :vals => [nil])
|
224
|
+
analysis.rows << CalculationRow.new(:key => "OISBT ($MM)", :vals => [nil])
|
225
|
+
end
|
226
|
+
analysis.rows << CalculationRow.new(:key => "Core OI ($MM)", :vals => [nil])
|
227
|
+
analysis.rows << CalculationRow.new(:key => "OI ($MM)", :vals => [nil])
|
228
|
+
analysis.rows << CalculationRow.new(:key => "FI ($MM)", :vals => [nil])
|
229
|
+
analysis.rows << CalculationRow.new(:key => "NI ($MM)", :vals => [nil])
|
230
|
+
analysis.rows << CalculationRow.new(:key => "Gross Margin", :vals => [nil])
|
231
|
+
analysis.rows << CalculationRow.new(:key => "Sales PM", :vals => [nil])
|
232
|
+
analysis.rows << CalculationRow.new(:key => "Operating PM", :vals => [nil])
|
233
|
+
analysis.rows << CalculationRow.new(:key => "FI / Sales", :vals => [nil])
|
234
|
+
analysis.rows << CalculationRow.new(:key => "NI / Sales", :vals => [nil])
|
235
|
+
analysis.rows << CalculationRow.new(:key => "Sales / NOA", :vals => [nil])
|
236
|
+
analysis.rows << CalculationRow.new(:key => "FI / NFA", :vals => [nil])
|
237
|
+
analysis.rows << CalculationRow.new(:key => "Revenue Growth", :vals => [nil])
|
238
|
+
analysis.rows << CalculationRow.new(:key => "Core OI Growth", :vals => [nil])
|
239
|
+
analysis.rows << CalculationRow.new(:key => "OI Growth", :vals => [nil])
|
240
|
+
analysis.rows << CalculationRow.new(:key => "ReOI ($MM)", :vals => [nil])
|
241
|
+
|
242
|
+
return analysis
|
243
|
+
end
|
244
|
+
|
245
|
+
def analysis(re_bs, prev_re_is, prev_re_bs)
|
246
|
+
analysis = CalculationSummary.new
|
247
|
+
analysis.title = ""
|
248
|
+
analysis.rows = []
|
249
|
+
|
250
|
+
if re_bs.nil?
|
251
|
+
analysis.header_row = CalculationHeader.new(:key => "", :vals => ["Unknown..."])
|
252
|
+
else
|
253
|
+
analysis.header_row = CalculationHeader.new(:key => "", :vals => [re_bs.period.to_pretty_s])
|
254
|
+
end
|
255
|
+
|
256
|
+
analysis.rows << CalculationRow.new(:key => "Revenue ($MM)", :vals => [operating_revenues.total.to_nearest_million])
|
257
|
+
if Config.income_detail_enabled?
|
258
|
+
analysis.rows << CalculationRow.new(:key => "COGS ($MM)", :vals => [@cogs.total.to_nearest_million])
|
259
|
+
analysis.rows << CalculationRow.new(:key => "GM ($MM)", :vals => [@gm.to_nearest_million])
|
260
|
+
analysis.rows << CalculationRow.new(:key => "OE ($MM)", :vals => [@oe.total.to_nearest_million])
|
261
|
+
analysis.rows << CalculationRow.new(:key => "OISBT ($MM)", :vals => [income_from_sales_before_tax.total.to_nearest_million])
|
262
|
+
end
|
263
|
+
analysis.rows << CalculationRow.new(:key => "Core OI ($MM)", :vals => [income_from_sales_after_tax.total.to_nearest_million])
|
264
|
+
analysis.rows << CalculationRow.new(:key => "OI ($MM)", :vals => [operating_income_after_tax.total.to_nearest_million])
|
265
|
+
analysis.rows << CalculationRow.new(:key => "FI ($MM)", :vals => [net_financing_income.total.to_nearest_million])
|
266
|
+
analysis.rows << CalculationRow.new(:key => "NI ($MM)", :vals => [comprehensive_income.total.to_nearest_million])
|
267
|
+
analysis.rows << CalculationRow.new(:key => "Gross Margin", :vals => [gross_margin])
|
268
|
+
analysis.rows << CalculationRow.new(:key => "Sales PM", :vals => [sales_profit_margin])
|
269
|
+
analysis.rows << CalculationRow.new(:key => "Operating PM", :vals => [operating_profit_margin])
|
270
|
+
analysis.rows << CalculationRow.new(:key => "FI / Sales", :vals => [fi_over_sales])
|
271
|
+
analysis.rows << CalculationRow.new(:key => "NI / Sales", :vals => [ni_over_sales])
|
272
|
+
|
273
|
+
if !prev_re_bs.nil? && !prev_re_is.nil?
|
274
|
+
analysis.rows << CalculationRow.new(:key => "Sales / NOA", :vals => [sales_over_noa(prev_re_bs)])
|
275
|
+
analysis.rows << CalculationRow.new(:key => "FI / NFA", :vals => [fi_over_nfa( prev_re_bs)])
|
276
|
+
analysis.rows << CalculationRow.new(:key => "Revenue Growth",:vals => [revenue_growth(prev_re_is)])
|
277
|
+
analysis.rows << CalculationRow.new(:key => "Core OI Growth",:vals => [core_oi_growth(prev_re_is)])
|
278
|
+
analysis.rows << CalculationRow.new(:key => "OI Growth", :vals => [oi_growth( prev_re_is)])
|
279
|
+
analysis.rows << CalculationRow.new(:key => "ReOI ($MM)", :vals => [re_oi( prev_re_bs).to_nearest_million])
|
280
|
+
else
|
281
|
+
analysis.rows << CalculationRow.new(:key => "Sales / NOA", :vals => [nil])
|
282
|
+
analysis.rows << CalculationRow.new(:key => "FI / NFA", :vals => [nil])
|
283
|
+
analysis.rows << CalculationRow.new(:key => "Revenue Growth",:vals => [nil])
|
284
|
+
analysis.rows << CalculationRow.new(:key => "Core OI Growth",:vals => [nil])
|
285
|
+
analysis.rows << CalculationRow.new(:key => "OI Growth", :vals => [nil])
|
286
|
+
analysis.rows << CalculationRow.new(:key => "ReOI ($MM)", :vals => [nil])
|
287
|
+
end
|
288
|
+
|
289
|
+
return analysis
|
290
|
+
end
|
291
|
+
|
292
|
+
def self.forecast_next(period, policy, last_re_bs, last_re_is)
|
293
|
+
operating_revenues = last_re_is.operating_revenues.total * (1.0 + Rate.new(policy.revenue_growth).yearly_to_quarterly)
|
294
|
+
income_from_sales_after_tax = operating_revenues * policy.sales_pm
|
295
|
+
net_financing_income = last_re_bs.net_financial_assets.total * Ratio.new(policy.fi_over_nfa).yearly_to_quarterly
|
296
|
+
|
297
|
+
comprehensive_income = income_from_sales_after_tax + net_financing_income
|
298
|
+
|
299
|
+
ForecastedReformulatedIncomeStatement.new(period, operating_revenues,
|
300
|
+
income_from_sales_after_tax,
|
301
|
+
net_financing_income, comprehensive_income)
|
302
|
+
end
|
303
|
+
|
304
|
+
private
|
305
|
+
|
306
|
+
def annualize_rate(prev, rate)
|
307
|
+
from_days = case
|
308
|
+
when prev.period.is_instant?
|
309
|
+
Xbrlware::DateUtil.days_between(prev.period.value, @period.value["end_date"])
|
310
|
+
when prev.period.is_duration?
|
311
|
+
Xbrlware::DateUtil.days_between(prev.period.value["end_date"], @period.value["end_date"])
|
312
|
+
end
|
313
|
+
Rate.new(rate).annualize(from_days, to_days=365)
|
314
|
+
end
|
315
|
+
|
316
|
+
def deannualize_rate(prev, rate)
|
317
|
+
to_days = case
|
318
|
+
when prev.period.is_instant?
|
319
|
+
Xbrlware::DateUtil.days_between(prev.period.value, @period.value["end_date"])
|
320
|
+
when prev.period.is_duration?
|
321
|
+
Xbrlware::DateUtil.days_between(prev.period.value["end_date"], @period.value["end_date"])
|
322
|
+
end
|
323
|
+
Rate.new(rate).annualize(from_days=365, to_days)
|
324
|
+
end
|
325
|
+
|
326
|
+
end
|
327
|
+
|
328
|
+
class ForecastedReformulatedIncomeStatement < ReformulatedIncomeStatement
|
329
|
+
def initialize(period, operating_revenues, income_from_sales_after_tax, net_financing_income, comprehensive_income)
|
330
|
+
@period = period
|
331
|
+
@orev = operating_revenues
|
332
|
+
@income_from_sales_after_tax = income_from_sales_after_tax
|
333
|
+
@net_financing_income = net_financing_income
|
334
|
+
@comprehensive_income = comprehensive_income
|
335
|
+
end
|
336
|
+
|
337
|
+
def -(ris2)
|
338
|
+
raise RuntimeError.new("not implmeneted")
|
339
|
+
end
|
340
|
+
|
341
|
+
def operating_revenues
|
342
|
+
cs = FinModeling::CalculationSummary.new
|
343
|
+
cs.title = "Operating Revenues"
|
344
|
+
cs.rows = [ CalculationRow.new(:key => "Operating Revenues (OR)", :vals => [@orev] ) ]
|
345
|
+
return cs
|
346
|
+
end
|
347
|
+
|
348
|
+
def cost_of_revenues
|
349
|
+
nil
|
350
|
+
end
|
351
|
+
|
352
|
+
def gross_revenue
|
353
|
+
nil
|
354
|
+
end
|
355
|
+
|
356
|
+
def operating_expenses
|
357
|
+
nil
|
358
|
+
end
|
359
|
+
|
360
|
+
def income_from_sales_before_tax
|
361
|
+
nil
|
362
|
+
end
|
363
|
+
|
364
|
+
def income_from_sales_after_tax
|
365
|
+
cs = FinModeling::CalculationSummary.new
|
366
|
+
cs.title = "Operating Income from sales, after tax (OISAT)"
|
367
|
+
cs.rows = [ CalculationRow.new(:key => "Operating income from sales (after tax)", :vals => [@income_from_sales_after_tax] ) ]
|
368
|
+
return cs
|
369
|
+
end
|
370
|
+
|
371
|
+
def operating_income_after_tax
|
372
|
+
income_from_sales_after_tax # this simplified version assumes no non-sales operating income
|
373
|
+
end
|
374
|
+
|
375
|
+
def net_financing_income
|
376
|
+
cs = FinModeling::CalculationSummary.new
|
377
|
+
cs.title = "Net financing income, after tax (NFI)"
|
378
|
+
cs.rows = [ CalculationRow.new(:key => "Net financing income", :vals => [@net_financing_income] ) ]
|
379
|
+
return cs
|
380
|
+
end
|
381
|
+
|
382
|
+
def comprehensive_income
|
383
|
+
cs = FinModeling::CalculationSummary.new
|
384
|
+
cs.title = "Comprehensive Income (CI)"
|
385
|
+
cs.rows = [ CalculationRow.new(:key => "Comprehensive income", :vals => [@comprehensive_income] ) ]
|
386
|
+
return cs
|
387
|
+
end
|
388
|
+
|
389
|
+
def analysis(re_bs, prev_re_is, prev_re_bs)
|
390
|
+
analysis = CalculationSummary.new
|
391
|
+
analysis.title = ""
|
392
|
+
analysis.rows = []
|
393
|
+
|
394
|
+
if re_bs.nil?
|
395
|
+
analysis.header_row = CalculationHeader.new(:key => "", :vals => ["Unknown..."])
|
396
|
+
else
|
397
|
+
analysis.header_row = CalculationHeader.new(:key => "", :vals => [re_bs.period.to_pretty_s + "E"])
|
398
|
+
end
|
399
|
+
|
400
|
+
analysis.rows << CalculationRow.new(:key => "Revenue ($MM)", :vals => [operating_revenues.total.to_nearest_million])
|
401
|
+
if Config.income_detail_enabled?
|
402
|
+
analysis.rows << CalculationRow.new(:key => "COGS ($MM)", :vals => [nil])
|
403
|
+
analysis.rows << CalculationRow.new(:key => "GM ($MM)", :vals => [nil])
|
404
|
+
analysis.rows << CalculationRow.new(:key => "OE ($MM)", :vals => [nil])
|
405
|
+
analysis.rows << CalculationRow.new(:key => "OISBT ($MM)", :vals => [nil])
|
406
|
+
end
|
407
|
+
analysis.rows << CalculationRow.new(:key => "Core OI ($MM)", :vals => [income_from_sales_after_tax.total.to_nearest_million])
|
408
|
+
analysis.rows << CalculationRow.new(:key => "OI ($MM)", :vals => [nil])
|
409
|
+
analysis.rows << CalculationRow.new(:key => "FI ($MM)", :vals => [net_financing_income.total.to_nearest_million])
|
410
|
+
analysis.rows << CalculationRow.new(:key => "NI ($MM)", :vals => [comprehensive_income.total.to_nearest_million])
|
411
|
+
analysis.rows << CalculationRow.new(:key => "Gross Margin", :vals => [nil])
|
412
|
+
analysis.rows << CalculationRow.new(:key => "Sales PM", :vals => [sales_profit_margin])
|
413
|
+
analysis.rows << CalculationRow.new(:key => "Operating PM", :vals => [nil])
|
414
|
+
analysis.rows << CalculationRow.new(:key => "FI / Sales", :vals => [fi_over_sales])
|
415
|
+
analysis.rows << CalculationRow.new(:key => "NI / Sales", :vals => [ni_over_sales])
|
416
|
+
|
417
|
+
if !prev_re_bs.nil? && !prev_re_is.nil?
|
418
|
+
analysis.rows << CalculationRow.new(:key => "Sales / NOA", :vals => [sales_over_noa(prev_re_bs)])
|
419
|
+
analysis.rows << CalculationRow.new(:key => "FI / NFA", :vals => [fi_over_nfa( prev_re_bs)])
|
420
|
+
analysis.rows << CalculationRow.new(:key => "Revenue Growth",:vals => [revenue_growth(prev_re_is)])
|
421
|
+
analysis.rows << CalculationRow.new(:key => "Core OI Growth",:vals => [core_oi_growth(prev_re_is)])
|
422
|
+
analysis.rows << CalculationRow.new(:key => "OI Growth", :vals => [nil])
|
423
|
+
analysis.rows << CalculationRow.new(:key => "ReOI ($MM)", :vals => [re_oi( prev_re_bs).to_nearest_million])
|
424
|
+
else
|
425
|
+
analysis.rows << CalculationRow.new(:key => "Sales / NOA", :vals => [nil])
|
426
|
+
analysis.rows << CalculationRow.new(:key => "FI / NFA", :vals => [nil])
|
427
|
+
analysis.rows << CalculationRow.new(:key => "Revenue Growth",:vals => [nil])
|
428
|
+
analysis.rows << CalculationRow.new(:key => "Core OI Growth",:vals => [nil])
|
429
|
+
analysis.rows << CalculationRow.new(:key => "OI Growth", :vals => [nil])
|
430
|
+
analysis.rows << CalculationRow.new(:key => "ReOI ($MM)", :vals => [nil])
|
431
|
+
end
|
432
|
+
|
433
|
+
return analysis
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class String
|
2
|
+
def fixed_width_left_justify(width)
|
3
|
+
return self[0..(width-1 )] if self.length == width
|
4
|
+
return self[0..(width-1-3)]+"..." if self.length > width
|
5
|
+
return self + (" " * (width - self.length))
|
6
|
+
end
|
7
|
+
|
8
|
+
def fixed_width_right_justify(width)
|
9
|
+
return self[(-width )..-1] if self.length == width
|
10
|
+
return "..."+self[(-width+3)..-1] if self.length > width
|
11
|
+
return (" " * (width - self.length)) + self
|
12
|
+
end
|
13
|
+
|
14
|
+
def with_thousands_separators
|
15
|
+
self.reverse.scan(/(?:\d*\.)?\d{1,3}-?/).join(',').reverse
|
16
|
+
end
|
17
|
+
|
18
|
+
def cap_decimals(num_decimals)
|
19
|
+
r = Regexp.new('(.*\.[0-9]{' + num_decimals.to_s + '})[0-9]*')
|
20
|
+
self.gsub(r, '\1')
|
21
|
+
end
|
22
|
+
|
23
|
+
def matches_regexes?(regexes)
|
24
|
+
return regexes.inject(false){ |matches, regex| matches or regex =~ self }
|
25
|
+
end
|
26
|
+
end
|
data/lib/finmodeling.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'digest'
|
3
|
+
require 'sec_query'
|
4
|
+
require 'edgar'
|
5
|
+
|
6
|
+
require 'xbrlware-ruby19'
|
7
|
+
require 'xbrlware-extras'
|
8
|
+
|
9
|
+
require 'naive_bayes'
|
10
|
+
require 'statsample'
|
11
|
+
|
12
|
+
require 'finmodeling/float_helpers'
|
13
|
+
require 'finmodeling/string_helpers'
|
14
|
+
require 'finmodeling/factory'
|
15
|
+
|
16
|
+
require 'finmodeling/paths'
|
17
|
+
|
18
|
+
require 'finmodeling/has_string_classifer'
|
19
|
+
|
20
|
+
require 'finmodeling/period_array'
|
21
|
+
require 'finmodeling/rate'
|
22
|
+
require 'finmodeling/ratio'
|
23
|
+
require 'finmodeling/company'
|
24
|
+
|
25
|
+
require 'finmodeling/company_filings'
|
26
|
+
require 'finmodeling/company_filing'
|
27
|
+
require 'finmodeling/annual_report_filing'
|
28
|
+
require 'finmodeling/quarterly_report_filing'
|
29
|
+
|
30
|
+
require 'finmodeling/array_with_stats'
|
31
|
+
require 'finmodeling/calculation_summary'
|
32
|
+
|
33
|
+
require 'finmodeling/can_classify_rows'
|
34
|
+
require 'finmodeling/can_cache_classifications'
|
35
|
+
require 'finmodeling/can_cache_summaries'
|
36
|
+
|
37
|
+
require 'finmodeling/assets_item_vectors'
|
38
|
+
require 'finmodeling/assets_item'
|
39
|
+
require 'finmodeling/liabs_and_equity_item_vectors'
|
40
|
+
require 'finmodeling/liabs_and_equity_item'
|
41
|
+
require 'finmodeling/income_statement_item_vectors'
|
42
|
+
require 'finmodeling/income_statement_item'
|
43
|
+
require 'finmodeling/cash_change_item_vectors'
|
44
|
+
require 'finmodeling/cash_change_item'
|
45
|
+
|
46
|
+
require 'finmodeling/company_filing_calculation'
|
47
|
+
require 'finmodeling/balance_sheet_calculation'
|
48
|
+
require 'finmodeling/assets_calculation'
|
49
|
+
require 'finmodeling/liabs_and_equity_calculation'
|
50
|
+
require 'finmodeling/income_statement_calculation'
|
51
|
+
require 'finmodeling/net_income_calculation'
|
52
|
+
require 'finmodeling/cash_flow_statement_calculation'
|
53
|
+
require 'finmodeling/cash_change_calculation'
|
54
|
+
|
55
|
+
require 'finmodeling/reformulated_income_statement'
|
56
|
+
require 'finmodeling/reformulated_balance_sheet'
|
57
|
+
require 'finmodeling/reformulated_cash_flow_statement'
|
58
|
+
|
59
|
+
require 'finmodeling/config'
|
60
|
+
|
61
|
+
require 'finmodeling/classifiers'
|
62
|
+
FinModeling::Classifiers.train
|
63
|
+
|
64
|
+
require 'finmodeling/balance_sheet_analyses'
|
65
|
+
require 'finmodeling/income_statement_analyses'
|
66
|
+
|
67
|
+
require 'finmodeling/generic_forecasting_policy'
|
68
|
+
require 'finmodeling/constant_forecasting_policy'
|
69
|
+
require 'finmodeling/forecasts'
|
70
|
+
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# annual_report_filing_spec.rb
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe FinModeling::AnnualReportFiling do
|
6
|
+
before(:all) do
|
7
|
+
company = FinModeling::Company.new(FinModeling::Mocks::Entity.new)
|
8
|
+
filing_url = company.annual_reports.last.link
|
9
|
+
FinModeling::Config::disable_caching
|
10
|
+
@filing = FinModeling::AnnualReportFiling.download(filing_url)
|
11
|
+
end
|
12
|
+
|
13
|
+
after(:all) do
|
14
|
+
FinModeling::Config::enable_caching
|
15
|
+
end
|
16
|
+
|
17
|
+
subject { @filing }
|
18
|
+
its(:balance_sheet) { should be_a FinModeling::BalanceSheetCalculation }
|
19
|
+
its(:income_statement) { should be_a FinModeling::IncomeStatementCalculation }
|
20
|
+
its(:cash_flow_statement) { should be_a FinModeling::CashFlowStatementCalculation }
|
21
|
+
|
22
|
+
its(:is_valid?) { should == (@filing.income_statement.is_valid? && @filing.balance_sheet.is_valid? && @filing.cash_flow_statement.is_valid?) }
|
23
|
+
|
24
|
+
describe "write_constructor" do
|
25
|
+
before(:all) do
|
26
|
+
file_name = "/tmp/finmodeling-annual-rpt.rb"
|
27
|
+
schema_version_item_name = "@schema_version"
|
28
|
+
item_name = "@annual_rpt"
|
29
|
+
file = File.open(file_name, "w")
|
30
|
+
@filing.write_constructor(file, item_name)
|
31
|
+
file.close
|
32
|
+
|
33
|
+
eval(File.read(file_name))
|
34
|
+
|
35
|
+
@schema_version = eval(schema_version_item_name)
|
36
|
+
@loaded_filing = eval(item_name)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "writes itself to a file, and saves a schema version of 1.1" do
|
40
|
+
@schema_version.should be == 1.1
|
41
|
+
end
|
42
|
+
|
43
|
+
it "writes itself to a file, and when reloaded, has the same periods" do
|
44
|
+
expected_periods = @filing.balance_sheet.periods.map{|x| x.to_pretty_s}.join(',')
|
45
|
+
@loaded_filing.balance_sheet.periods.map{|x| x.to_pretty_s}.join(',').should == expected_periods
|
46
|
+
end
|
47
|
+
it "writes itself to a file, and when reloaded, has the same net operating assets" do
|
48
|
+
period = @filing.balance_sheet.periods.last
|
49
|
+
expected_noa = @filing.balance_sheet.reformulated(period).net_operating_assets.total
|
50
|
+
@loaded_filing.balance_sheet.reformulated(period).net_operating_assets.total.should be_within(1.0).of(expected_noa)
|
51
|
+
end
|
52
|
+
it "writes itself to a file, and when reloaded, has the same net financing income" do
|
53
|
+
period = @filing.income_statement.periods.last
|
54
|
+
expected_nfi = @filing.income_statement.reformulated(period).net_financing_income.total
|
55
|
+
@loaded_filing.income_statement.reformulated(period).net_financing_income.total.should be_within(1.0).of(expected_nfi)
|
56
|
+
end
|
57
|
+
it "writes itself to a file, and when reloaded, has the same net change in cash" do
|
58
|
+
period = @filing.cash_flow_statement.periods.last
|
59
|
+
expected_cash_change = @filing.cash_flow_statement.cash_change_calculation.summary(:period=>period).total
|
60
|
+
@loaded_filing.cash_flow_statement.cash_change_calculation.summary(:period=>period).total.should be_within(1.0).of(expected_cash_change)
|
61
|
+
end
|
62
|
+
it "writes itself to a file, and when reloaded, has the same disclosures" do
|
63
|
+
period = @filing.disclosures.first.periods.last
|
64
|
+
expected_total = @filing.disclosures.first.summary(:period=>period).total
|
65
|
+
@loaded_filing.disclosures.first.summary(:period=>period).total.should == expected_total
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# assets_calculation_spec.rb
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe FinModeling::AssetsCalculation do
|
6
|
+
before(:all) do
|
7
|
+
google_2011_annual_rpt = "http://www.sec.gov/Archives/edgar/data/1288776/000119312512025336/0001193125-12-025336-index.htm"
|
8
|
+
filing = FinModeling::AnnualReportFiling.download google_2011_annual_rpt
|
9
|
+
@bal_sheet = filing.balance_sheet
|
10
|
+
|
11
|
+
@period = @bal_sheet.periods.last
|
12
|
+
@a = @bal_sheet.assets_calculation
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "summary" do
|
16
|
+
it "only requires a period (knows how debts/credits work and whether to flip the total)" do
|
17
|
+
@a.summary(:period=>@period).should be_an_instance_of FinModeling::CalculationSummary
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|