fundamentalista 0.2.0 → 0.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36260d14ca8a99975cc3c9717ecd1fa5694f2e2d29144aaeb8d51e9eed3a25a5
4
- data.tar.gz: 920282a4dd650e6ed172638e02b85c37b3ac6e0166af5f7a837b4e69003d1c59
3
+ metadata.gz: 37f787ea99f5a949329fb943c8994bc1fa3cb3b9f0ed357757926b4bb661fb57
4
+ data.tar.gz: f974e488c25c1b0628e9c6fbca2400b6aa86c99705d8d274d008591a0a93d52e
5
5
  SHA512:
6
- metadata.gz: 698002a96de44e7c842e619fbce287758f2478f56f0e0a3c1fcbb131a635e88316b9609bfba710c7223627f6ea7ba67b5a3ad5cc5a9296cf34b061a65b76956f
7
- data.tar.gz: 675e708b8b952f95aa50ef374165b37fa70f9d7d6e217ee8b14fb7a8157fec4c1402f8a0703aba8d4808fa33e0202a75eeafc43e2fd76a8bdba358b63f044763
6
+ metadata.gz: a8f4e210ea59fac412647858620bd23f880785fb723c902770e81b295bdfd0671884affb0b1a849e165f49f0d2dc90660c5978374f673b5038b8e5ef6002cf6b
7
+ data.tar.gz: 438bf83861f879aa1c4f4b47468ffdb974ab1d3700aaa5ea8ec8375a8da891e689bc21bbc7ce11b1e50b290351342da399040e185fd5c6f06d4e2392389e46b5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - Insurance lines and ratios: premiums, claims, loss, expense and combined ratios, investment yield and the float, read from the concepts property and casualty insurers report.
6
+
3
7
  ## 0.2.0
4
8
 
5
9
  - Daily prices from FMP as a `PriceHistory` with dividend adjusted returns, the valuation of every fiscal year at its closing price, and the company beta.
data/README.md CHANGED
@@ -113,7 +113,22 @@ jpm.ratios.cost_of_risk # credit provisions over average loans
113
113
  jpm.ratios.allowance_to_loans
114
114
  ```
115
115
 
116
- The banking ratios appear in `ratios.to_h` only for banks, and the industrial ones that need inventory, cost of revenue or a classified balance sheet come back `nil` for them. Quarterly ratios are per quarter, not annualized; use the trailing twelve months for returns.
116
+ The banking and insurance ratios appear in `ratios.to_h` only for banks and insurers, and the industrial ones that need inventory, cost of revenue or a classified balance sheet come back `nil` for them. Quarterly ratios are per quarter, not annualized; use the trailing twelve months for returns.
117
+
118
+ ## Insurers
119
+
120
+ Insurers earn premiums and invest the float, so their lines live on `period.insurance`: premiums earned and written, claims incurred, total benefits and expenses, investment income, investments, reserves, unearned premiums, premiums receivable and reinsurance recoverables. `period.insurer?` says whether the company earns premiums, and `insurance.float` is Buffett's: reserves plus unearned premiums, less what policyholders and reinsurers still owe.
121
+
122
+ ```ruby
123
+ trv = Fundamentalista.company("TRV").financials.latest
124
+ trv.ratios.loss_ratio # claims incurred over premiums earned
125
+ trv.ratios.combined_ratio # benefits, losses and expenses over premiums earned; below one, underwriting makes money
126
+ trv.ratios.expense_ratio # the difference between the two
127
+ trv.ratios.investment_yield # investment income over average investments
128
+ trv.ratios.float_to_equity
129
+ ```
130
+
131
+ These are property and casualty concepts. For life insurers, whose revenue is mostly investment income and whose reserves are future policy benefits, the combined ratio and the float mislead; read their investment yield and return on equity instead.
117
132
 
118
133
  ## Ratios
119
134
 
@@ -92,7 +92,8 @@ module Fundamentalista
92
92
  currency: quarters.first.currency, balance: quarters.first.balance,
93
93
  income: IncomeStatement.sum(quarters.map(&:income)),
94
94
  cash_flow: CashFlowStatement.sum(quarters.map(&:cash_flow)),
95
- banking: Banking.sum(quarters.map(&:banking))
95
+ banking: Banking.sum(quarters.map(&:banking)),
96
+ insurance: Insurance.sum(quarters.map(&:insurance))
96
97
  )
97
98
  end
98
99
 
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fundamentalista
4
+ # The lines an insurer reports that the three classic statements do not
5
+ # carry: premiums, claims, investment income, reserves and the balances
6
+ # behind the float. Empty for companies that are not insurers. All in
7
+ # the reporting currency.
8
+ class Insurance < Statement
9
+ field :premiums_earned, :premiums_written, :claims_incurred, :benefits_and_expenses, :investment_income,
10
+ :investments, :reserves, :unearned_premiums, :premiums_receivable, :reinsurance_recoverables
11
+
12
+ INSTANT = %i[investments reserves unearned_premiums premiums_receivable reinsurance_recoverables].freeze
13
+
14
+ # Adds up +statements+, keeping the latest balances instead.
15
+ def self.sum(statements)
16
+ summed = super
17
+ balances = INSTANT.to_h { |field| [field, statements.first.public_send(field)] }
18
+ new(**summed.to_h, **balances)
19
+ end
20
+
21
+ # Whether the company earns premiums, the mark of an insurer.
22
+ def insurer?
23
+ !premiums_earned.nil? && !premiums_earned.zero?
24
+ end
25
+
26
+ # Buffett's float: reserves and unearned premiums, less what
27
+ # policyholders and reinsurers still owe. The money an insurer invests
28
+ # before it pays claims.
29
+ def float
30
+ return nil if reserves.nil?
31
+
32
+ reserves + (unearned_premiums || 0) - (premiums_receivable || 0) - (reinsurance_recoverables || 0)
33
+ end
34
+ end
35
+ end
@@ -27,6 +27,9 @@ module Fundamentalista
27
27
  # The Banking lines, empty for companies that are not banks.
28
28
  attr_reader :banking
29
29
 
30
+ # The Insurance lines, empty for companies that are not insurers.
31
+ attr_reader :insurance
32
+
30
33
  # Where each line item came from, keyed by item, as Source objects.
31
34
  # Empty when the provider does not say.
32
35
  attr_reader :sources
@@ -37,14 +40,15 @@ module Fundamentalista
37
40
  # The period before this one, or +nil+.
38
41
  attr_accessor :prior
39
42
 
40
- def initialize(fiscal_year:, ended_on:, income:, balance:, cash_flow:, banking: Banking.new, sources: {},
41
- type: :annual, quarter: nil, currency: 'USD', prior: nil)
43
+ def initialize(fiscal_year:, ended_on:, income:, balance:, cash_flow:, banking: Banking.new, insurance: Insurance.new,
44
+ sources: {}, type: :annual, quarter: nil, currency: 'USD', prior: nil)
42
45
  @fiscal_year = fiscal_year
43
46
  @ended_on = ended_on
44
47
  @income = income
45
48
  @balance = balance
46
49
  @cash_flow = cash_flow
47
50
  @banking = banking
51
+ @insurance = insurance
48
52
  @sources = sources
49
53
  @type = type
50
54
  @quarter = quarter
@@ -69,6 +73,11 @@ module Fundamentalista
69
73
  banking.bank?
70
74
  end
71
75
 
76
+ # Whether the company is an insurer, judged by premiums earned.
77
+ def insurer?
78
+ insurance.insurer?
79
+ end
80
+
72
81
  # Returns the Source of the line item +name+, or +nil+.
73
82
  def source(name)
74
83
  sources[name]
@@ -90,7 +99,8 @@ module Fundamentalista
90
99
  # Returns the period and its statements as a Hash.
91
100
  def to_h
92
101
  { fiscal_year: fiscal_year, quarter: quarter, type: type, ended_on: ended_on, currency: currency,
93
- income: income.to_h, balance: balance.to_h, cash_flow: cash_flow.to_h, banking: banking.to_h }
102
+ income: income.to_h, balance: balance.to_h, cash_flow: cash_flow.to_h, banking: banking.to_h,
103
+ insurance: insurance.to_h }
94
104
  end
95
105
 
96
106
  # The days the period spans, for turning turnover into days.
@@ -136,7 +146,7 @@ module Fundamentalista
136
146
  return owner_earnings if name == :owner_earnings
137
147
  return per_share(name.to_s.delete_suffix('_per_share').to_sym) if name.end_with?('_per_share')
138
148
 
139
- statement = [income, balance, cash_flow, banking].find { |candidate| candidate.respond_to?(name) }
149
+ statement = [income, balance, cash_flow, banking, insurance].find { |candidate| candidate.respond_to?(name) }
140
150
  raise ArgumentError, "Unknown metric #{name.inspect}" unless statement
141
151
 
142
152
  statement.public_send(name)
@@ -111,7 +111,22 @@ module Fundamentalista
111
111
  # by fiscal year end, used when the balance sheet lacks the item.
112
112
  COVER = { shares_outstanding: 'EntityCommonStockSharesOutstanding' }.freeze
113
113
 
114
- INSTANT = (BALANCE.keys + Banking::INSTANT).freeze
114
+ INSURANCE = {
115
+ premiums_earned: %w[PremiumsEarnedNet DirectPremiumsEarned],
116
+ premiums_written: %w[PremiumsWrittenNet DirectPremiumsWritten],
117
+ claims_incurred: %w[PolicyholderBenefitsAndClaimsIncurredNet
118
+ LiabilityForUnpaidClaimsAndClaimsAdjustmentExpenseIncurredClaims1],
119
+ benefits_and_expenses: %w[BenefitsLossesAndExpenses],
120
+ investment_income: %w[NetInvestmentIncome GrossInvestmentIncomeOperating],
121
+ investments: %w[Investments],
122
+ reserves: %w[LiabilityForFuturePolicyBenefitsAndUnpaidClaimsAndClaimsAdjustmentExpense
123
+ LiabilityForClaimsAndClaimsAdjustmentExpense LiabilityForFuturePolicyBenefits],
124
+ unearned_premiums: %w[UnearnedPremiums],
125
+ premiums_receivable: %w[PremiumsReceivableAtCarryingValue],
126
+ reinsurance_recoverables: %w[ReinsuranceRecoverablesOnPaidAndUnpaidLosses]
127
+ }.freeze
128
+
129
+ INSTANT = (BALANCE.keys + Banking::INSTANT + Insurance::INSTANT).freeze
115
130
  end
116
131
  end
117
132
  end
@@ -47,7 +47,8 @@ module Fundamentalista
47
47
  private
48
48
 
49
49
  STATEMENTS = { income: [IncomeStatement, Tags::INCOME], balance: [BalanceSheet, Tags::BALANCE],
50
- cash_flow: [CashFlowStatement, Tags::CASH_FLOW], banking: [Banking, Tags::BANKING] }.freeze
50
+ cash_flow: [CashFlowStatement, Tags::CASH_FLOW], banking: [Banking, Tags::BANKING],
51
+ insurance: [Insurance, Tags::INSURANCE] }.freeze
51
52
  private_constant :STATEMENTS
52
53
 
53
54
  def annual_period(facts, year, ended_on)
@@ -24,7 +24,8 @@ module Fundamentalista
24
24
  cash_conversion_cycle].freeze
25
25
  CASH = %i[fcf_margin cash_conversion capex_to_revenue accrual_ratio].freeze
26
26
  BANKING = %i[net_interest_margin efficiency_ratio loan_to_deposit cost_of_risk allowance_to_loans].freeze
27
- ALL = (PROFITABILITY + LIQUIDITY + LEVERAGE + EFFICIENCY + WORKING_CAPITAL + CASH + BANKING).freeze
27
+ INSURANCE = %i[loss_ratio expense_ratio combined_ratio investment_yield float_to_equity].freeze
28
+ ALL = (PROFITABILITY + LIQUIDITY + LEVERAGE + EFFICIENCY + WORKING_CAPITAL + CASH + BANKING + INSURANCE).freeze
28
29
 
29
30
  attr_reader :period
30
31
 
@@ -182,15 +183,42 @@ module Fundamentalista
182
183
  Decimal.ratio(banking.allowance_for_credit_losses, banking.loans)
183
184
  end
184
185
 
186
+ # Claims incurred over premiums earned.
187
+ def loss_ratio
188
+ Decimal.ratio(insurance.claims_incurred, insurance.premiums_earned)
189
+ end
190
+
191
+ # Everything the insurer spends beyond claims, over premiums earned.
192
+ def expense_ratio
193
+ Decimal.subtract(combined_ratio, loss_ratio)
194
+ end
195
+
196
+ # Benefits, losses and expenses over premiums earned; below one the
197
+ # underwriting itself makes money.
198
+ def combined_ratio
199
+ Decimal.ratio(insurance.benefits_and_expenses, insurance.premiums_earned)
200
+ end
201
+
202
+ # Net investment income over average investments.
203
+ def investment_yield
204
+ Decimal.ratio(insurance.investment_income, average(:investments, of: :insurance))
205
+ end
206
+
207
+ # The float over equity, the leverage an insurer's investing runs on.
208
+ def float_to_equity
209
+ Decimal.ratio(insurance.float, balance.equity)
210
+ end
211
+
185
212
  # The DuPont decomposition of ROE: net margin times asset turnover times
186
213
  # the equity multiplier.
187
214
  def dupont
188
215
  { net_margin: net_margin, asset_turnover: asset_turnover, equity_multiplier: equity_multiplier, roe: roe }
189
216
  end
190
217
 
191
- # Returns every ratio keyed by name; the banking ones only for banks.
218
+ # Returns every ratio keyed by name; the banking ones only for banks,
219
+ # the insurance ones only for insurers.
192
220
  def to_h
193
- names = period.bank? ? ALL : ALL - BANKING
221
+ names = ALL - (period.bank? ? [] : BANKING) - (period.insurer? ? [] : INSURANCE)
194
222
  names.to_h { |name| [name, public_send(name)] }
195
223
  end
196
224
 
@@ -216,6 +244,10 @@ module Fundamentalista
216
244
  period.banking
217
245
  end
218
246
 
247
+ def insurance
248
+ period.insurance
249
+ end
250
+
219
251
  def average(field, of: :balance)
220
252
  current = period.public_send(of).public_send(field)
221
253
  return current unless period.prior
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Fundamentalista
4
4
  # The version of the fundamentalista gem, as a string.
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fundamentalista
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bruno Costanzo
@@ -92,6 +92,7 @@ files:
92
92
  - lib/fundamentalista/financials.rb
93
93
  - lib/fundamentalista/income_statement.rb
94
94
  - lib/fundamentalista/inspectable.rb
95
+ - lib/fundamentalista/insurance.rb
95
96
  - lib/fundamentalista/period.rb
96
97
  - lib/fundamentalista/price_history.rb
97
98
  - lib/fundamentalista/provider.rb