2 # This file is part of SMR.
4 # SMR is free software: you can redistribute it and/or modify it under the
5 # terms of the GNU General Public License as published by the Free Software
6 # Foundation, either version 3 of the License, or (at your option) any later
9 # SMR is distributed in the hope that it will be useful, but WITHOUT ANY
10 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License along with
14 # SMR. If not, see <http://www.gnu.org/licenses/>.
18 require 'cashflowitem'
19 require 'cashflowstream'
21 class SecurityBond < ActiveRecord::Base
22 include Smr::Extensions::HelperMethods
23 include Smr::Extensions::SecurityTypemodelMandatoryMethods
24 include Smr::Extensions::DateTimeWrapper
26 after_initialize :init_caches
28 self.inheritance_column = :none
29 has_one :Security, :foreign_key=>:id_security_bond, :inverse_of=>:SecurityBond
32 # types of bond supported (may alter behaviour of some methods)
33 # NOTE: do NOT change the relations! just ADD!
35 :fixed_rate=>0, :floating_rate=>1, :zero_rate=>2, :fixtofloat_rate=>3,
40 # validates :date_maturity, :date_first_coupon, :date_last_coupon, numericality: { greater_than_or_equal_to: 0 }
44 # Returns human readable String describing :type. Lookup may be given as
45 # symbol, as string or as numerical index.
46 def SecurityBond.describe_type(lookup)
48 :fixed_rate => 'Fixed Interest Rate',
49 :floating_rate => 'Floating Interest Rate',
50 :zero_rate => 'Zero Interest Rate',
51 :fixtofloat_rate => 'Fix-to-Float Interest Rate',
52 :convertible => 'Convertible',
55 if lookup.is_a?(Numeric) then
56 lookup = self.types.key(lookup) || ''
58 descriptions[lookup.to_sym]
62 # Describes #type of the instance object.
64 SecurityBond.describe_type(type)
68 # Returns humanreadable String describing the model itself.
69 def SecurityBond.describe
77 # Returns a collection of all types in their described form.
79 # Useful for Select boxes in forms, also see SecurityBond#describe_type. The
80 # :as_number option toggles whether symbols or integers are retured as
82 def self.types_for_form(options={ :as_number=>false })
83 SecurityBond.types.map { |t|
85 SecurityBond.describe_type(t.first),
86 options[:as_number] ? SecurityBond.types[t.first] : t.first
92 # interest calculation methods supported
93 enum interest_method: {
94 :act_act=>0, :act_366=>1, :act_365=>2, :act_360=>3,
95 :_30_360=>4, :_30_365=>5, :_30E_360=>6, :_30E_365=>7
99 # Returns human readable String describing the :interest_method. May be
100 # given as symbol, as string or as numerical index.
101 def SecurityBond.describe_interest_method(lookup)
103 :act_act => 'ICMA Rule: actual days per month and per year.',
104 :act_366 => 'Actual days per month, 366 days per year.',
105 :act_365 => 'English: actual days per month, 365 days per year.',
106 :act_360 => 'European / French: actual days per month, 360 days per year.',
107 :_30_366 => '30 days per month, 366 days per year.',
108 :_30_365 => '30 days per month, 365 days per year.',
109 :_30E_360 => 'German: 30 days per month, 360 days per year.',
110 :_30E_365 => 'German: 30 days per month, 365 days per year.',
113 if lookup.is_a?(Numeric) then
114 lookup = self.interest_methods.key(lookup) || ''
116 descriptions[lookup.to_sym]
120 # Describes #interest_method of the instance object.
121 def describe_interest_method
122 SecurityBond.describe_interest_method(interest_method)
126 # Returns a collection of all interest methods in their described form.
128 # Useful for Select boxes in forms, also see
129 # SecurityBond#describe_interest_method. The :as_number option toggles
130 # whether symbols or integers are retured as index.
131 def self.interest_methods_for_form(options={ :as_number=>false })
132 SecurityBond.interest_methods.map { |m|
134 SecurityBond.describe_interest_method(m.first),
135 options[:as_number] ? SecurityBond.interest_methods[m.first] : m.first
141 # overwrite :coupon attribute to provide a Percentage
143 Percentage read_attribute(:coupon)
146 write_attribute(:coupon, value.to_f)
150 # Human readable String composed of essential properties known by a
151 # SecurityBond on its own. Useful as part to compose the name of a
155 s << '%s' % coupon if coupon > 0
157 _redemption_year = if date_maturity <= 0
159 elsif redemption_installments == 1
160 time_maturity.strftime('%y')
163 (time_maturity - ((redemption_installments-1) * redemption_interval).months).year,
164 time_maturity.strftime('%y')
167 _issue_year = if date_issue > 0
169 else time_first_coupon.year end
170 s << '%s(%s)' % [_issue_year, _redemption_year]
173 s << case read_attribute(:type)
179 s << currency unless currency.blank? or currency == Smr::DEFAULT_CURRENCY
185 # find the next call date, based on :date_first_call and :call_interval
186 def time_next_call(date=Time.now)
187 return false if date_first_call == 0
190 next_call = time_first_call
191 until next_call >= date do
192 next_call = time_first_call + ((1.year / 12)* call_interval) * i # using call_interval.months here introduces many days offset
199 # mandatory method: Number of tradable items
201 return 0 if denomination==0 or issue_size==0
202 issue_size / denomination
206 # mandatory method: result based on :coupon and :quote.
207 def current_yield(quote=false)
209 return false if @cy_asked_for_lastquote
210 quote = self.Security.last_quote
211 @cy_asked_for_lastquote = true if quote.last.zero?
213 raise ':quote must be of Quote' if quote and not quote.is_a?(Quote)
214 return false if quote.last.zero?
215 return false if quote.last >= date_last_coupon
216 BigDecimal(coupon.to_s).as_percentage_of(quote.last)
220 # mandatory method: Yield to Call if :time_first_call is known
221 def yield_to_call(quote=false)
223 return false if @ytc_asked_for_lastquote
224 quote = self.Security.last_quote
225 @ytc_asked_for_lastquote = true if quote.last.zero?
227 raise ':quote must be of Quote' unless quote and quote.is_a?(Quote)
228 return false if quote.last.zero? or date_first_call <= 0 or call_interval == 0 or call_price == 0
229 return false if quote.date_last >= date_maturity
231 return @cache_ytc.fetch(quote.id) if @cache_ytc.has_key?(quote.id)
233 cf_stream_call = Smr::CashflowStream.new(
236 :payment_interval=>coupon_interval,
237 :split_amount_by_interval=>true,
238 :last_payment=>time_first_call,
239 :redemption_interval=>call_interval,
241 :redemption_price=>call_price,
242 :last_redemption=>time_next_call(quote.time_last),
243 :item_description=>'',
244 :id_for_caching=>('call%s%i' % [self.class, (id || 0)]).to_sym,
245 ) if date_first_call!=0 and call_price!=0
247 # purchase at quote is a cash-outflow
249 transactions << Finance::Transaction.new(
250 denomination * (quote.last/100) * -1 ,
251 :date=>quote.time_last
254 # coupons + redemptions will flow back in
255 cf_stream_call.get(denomination, :start=>quote.time_last).each do |c|
256 transactions << Finance::Transaction.new(
257 c.total, :date=>c.date
261 @cache_ytc[quote.id] = begin
262 if time_first_call < quote.time_last + 1.year
263 # absolute, when maturity is near
264 gain = transactions.inject(0){|sum,t| sum += t.amount}
265 BigDecimal(gain.to_s).as_percentage_of(transactions.first.amount.abs)
268 Percentage.new(transactions.xirr(0.1).effective)
271 # FIXME: see #yield_to_maturity below
272 #p '==> %s' % 'strange too'
278 # mandatory method: YTM if all coupons and par is payed as planned until
280 # False is returned if there is no Quote or :date_maturity is UNDEF (== 0)
281 def yield_to_maturity(quote=false)
283 return false if @ytm_asked_for_lastquote
284 quote = self.Security.last_quote
285 @ytm_asked_for_lastquote = true if quote.last.zero?
287 raise ':quote must be of Quote' unless quote.is_a?(Quote)
288 return false if quote.last.zero? or date_maturity <= 0 or redemption_installments == 0
289 return false if quote.date_last >= date_maturity
291 return @cache_ytm.fetch(quote.id) if @cache_ytm.has_key?(quote.id)
295 # purchase at quote is a cash-outflow
296 transactions << Finance::Transaction.new(
297 denomination * (quote.last/100) * -1 ,
298 :date=>quote.time_last
301 # coupons + redemptions will flow back in
302 @cf_stream.get(denomination, :start=>quote.time_last).each do |c|
303 transactions << Finance::Transaction.new(
304 c.total, :date=>c.date
308 @cache_ytm[quote.id] = begin
309 if time_maturity < quote.time_last + 1.year
310 # absolute, when maturity is near
311 gain = transactions.inject(0){|sum,t| sum += t.amount}
312 BigDecimal(gain.to_s).as_percentage_of(transactions.first.amount.abs)
315 Percentage.new(transactions.xirr(0.1).effective)
318 # FIXME: some Security records raise
319 # 'Singular Jacobian matrix. No change at x[0]'
320 # from jacobian.rb. Happens with /strange/ :transactions, ie. by
321 # giving very high coupon value, dont understand it really.
327 # mandatory method: True if :date is past :date_maturity
328 def is_expired?(date=Time.now)
329 true if date > time_maturity and date_maturity > 0
334 # mandatory method: return collection of Smr::CashflowItem with
335 # future/past/all cashflows created by this SecurityBond.
337 # The :amount option states the amount of Security items for which the
338 # cashflow is calculated. If not given, the smallest possible amount is
339 # used. This is :denomination in the case of bonds, one share in the case
342 # The :type option allows to filter what is returned. This model supports
343 # :none, :dividend_booking and :redemption_booking. Also see
344 # PositionRevision#types to have all types of cashflow known to
347 # In case :date_maturity is UNDEF (== 0) the cashflow is projected for 10
348 # years from :start_date.
349 def cashflow(options={:start_date=>false, :end_date=>false, :amount=>false, :type=>:none, :item_link=>false})
350 start_date = options[:start_date] || time_first_coupon
351 end_date = options[:end_date] if options[:end_date]
352 end_date = time_maturity + 1.day if not end_date and date_maturity>0
353 end_date = start_date + 10.years if not end_date
355 last_coupon_payment = (date_last_coupon > 0 ? time_last_coupon : start_date + 10.years)
356 nominal_value = options[:amount] || denomination
357 filter = options[:type] || :none
361 :start=>start_date, :end=>end_date,
362 :filter=>(options[:type] || :none),
363 :item_link=>options[:item_link]
368 # mandatory method: inspect :coupon_interval for happening multiple times a year
369 def has_subannual_payments?
374 # mandatory method: interest accrued since previous coupon
375 def accrued_interest(date=Time.now)
376 s = @cf_stream.get(denomination, :start=>(date - 1.year), :end=>date, :filter=>:dividend_booking)
380 days_since_coupon = (date - prev_coupon.date).to_i / 1.day
381 days_in_year = (Date.ordinal(date.year) - Date.ordinal(date.year-1)).to_i # implies ACT_ACT interest method
382 Percentage.new( read_attribute(:coupon) / 100.0 / days_in_year * days_since_coupon )
389 # create Smr::CashflowStream instances from SecurityBond attributes and a
390 # number of result caches
392 @cache_ytc = Hash.new
393 @cache_ytm = Hash.new
395 @ytc_asked_for_lastquote = false
396 @ytm_asked_for_lastquote = false
397 @cy_asked_for_lastquote = false
399 @cf_stream = Smr::CashflowStream.new(
402 :payment_interval=>coupon_interval,
403 :split_amount_by_interval=>true,
404 :last_payment=>(date_last_coupon <= 0 ? 0 : time_last_coupon),
405 :redemption_interval=>redemption_interval,
406 :redemptions=>redemption_installments,
407 :redemption_price=>redemption_price,
408 :last_redemption=>time_maturity,
409 :item_description=>self.Security.to_s,
410 :id_for_caching=>('%s%i' % [self.class, (id || 0)]).to_sym,