finmodeling 0.1
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.
- 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
|
+
|