manage bookmarks, bugfixes
[smr.git] / gui / app / models / security_bond.rb
blob5cf31288b1b7e0dcdc542ef88efa0a7ac7446f83
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
7 # version.
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/>.
16 require 'percentage'
17 require 'finance'
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
31     ##
32     # types of bond supported (may alter behaviour of some methods)
33     # NOTE: do NOT change the relations! just ADD!
34     enum type: {
35         :fixed_rate=>0, :floating_rate=>1, :zero_rate=>2, :fixtofloat_rate=>3,
36         :convertible=>4
37     }
39     # data validations
40 #    validates :date_maturity, :date_first_coupon, :date_last_coupon, numericality: { greater_than_or_equal_to: 0 }
43     ##
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)
47         descriptions = {
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',
53         }
55         if lookup.is_a?(Numeric) then
56             lookup = self.types.key(lookup) || ''
57         end
58         descriptions[lookup.to_sym]
59     end
61     ##
62     # Describes #type of the instance object.
63     def describe_type
64         SecurityBond.describe_type(type)
65     end
67     ##
68     # Returns humanreadable String describing the model itself.
69     def SecurityBond.describe
70         'Debt Ownership'
71     end
72     def describe
73         SecurityBond.describe
74     end
76     ##
77     # Returns a collection of all types in their described form.
78     #
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
81     # index.
82     def self.types_for_form(options={ :as_number=>false })
83         SecurityBond.types.map { |t|
84             [
85                 SecurityBond.describe_type(t.first),
86                 options[:as_number] ? SecurityBond.types[t.first] : t.first
87             ]
88         }
89     end
91     ##
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
96     }
98     ##
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)
102         descriptions = {
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.',
111         }
113         if lookup.is_a?(Numeric) then
114             lookup = self.interest_methods.key(lookup) || ''
115         end
116         descriptions[lookup.to_sym]
117     end
119     ##
120     # Describes #interest_method of the instance object.
121     def describe_interest_method
122         SecurityBond.describe_interest_method(interest_method)
123     end
125     ##
126     # Returns a collection of all interest methods in their described form.
127     #
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|
133             [
134                 SecurityBond.describe_interest_method(m.first),
135                 options[:as_number] ? SecurityBond.interest_methods[m.first] : m.first
136             ]
137         }
138     end
140     ##
141     # overwrite :coupon attribute to provide a Percentage
142     def coupon
143         Percentage read_attribute(:coupon)
144     end
145     def coupon=(value)
146         write_attribute(:coupon, value.to_f)
147     end
149     ##
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
152     # Security.
153     def to_s
154         s = Array.new
155         s << '%s' % coupon if coupon > 0
157         _redemption_year = if date_maturity <= 0
158             'UNDEF'
159         elsif redemption_installments == 1
160             time_maturity.strftime('%y')
161         else
162             '%s-%s' % [
163                 (time_maturity - ((redemption_installments-1) * redemption_interval).months).year,
164                 time_maturity.strftime('%y')
165             ]
166         end
167         _issue_year = if date_issue > 0
168            time_issue.year
169         else time_first_coupon.year end
170         s << '%s(%s)' % [_issue_year, _redemption_year]
173         s << case read_attribute(:type)
174             when 1 then 'FlR'
175             when 2 then 'Zero'
176             when 3 then 'FtF'
177             when 4 then 'CV'
178         end
179         s << currency unless currency.blank? or currency == Smr::DEFAULT_CURRENCY
181         s.join ' '
182     end
184     ##
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
189         i = 1
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
193             i += 1
194         end
195         next_call
196     end
198     ##
199     # mandatory method: Number of tradable items
200     def shares
201         return 0 if denomination==0 or issue_size==0
202         issue_size / denomination
203     end
205     ##
206     # mandatory method: result based on :coupon and :quote.
207     def current_yield(quote=false)
208         if not quote
209             return false if @cy_asked_for_lastquote
210             quote = self.Security.last_quote
211             @cy_asked_for_lastquote = true if quote.last.zero?
212         end
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)
217     end
219     ##
220     # mandatory method: Yield to Call if :time_first_call is known
221     def yield_to_call(quote=false)
222         if not quote
223             return false if @ytc_asked_for_lastquote
224             quote = self.Security.last_quote
225             @ytc_asked_for_lastquote = true if quote.last.zero?
226         end
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(
234             coupon,
235             time_first_coupon,
236             :payment_interval=>coupon_interval,
237             :split_amount_by_interval=>true,
238             :last_payment=>time_first_call,
239             :redemption_interval=>call_interval,
240             :redemptions=>1,
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
248         transactions = []
249         transactions << Finance::Transaction.new(
250             denomination * (quote.last/100) * -1 ,
251             :date=>quote.time_last
252         )
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
258             )
259         end
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)
266                else
267                    # annualized
268                    Percentage.new(transactions.xirr(0.1).effective)
269                end
270            rescue
271                # FIXME: see #yield_to_maturity below
272 #p '==> %s' %  'strange too'
273                false
274         end
275     end
277     ##
278     # mandatory method: YTM if all coupons and par is payed as planned until
279     # :date_maturity
280     # False is returned if there is no Quote or :date_maturity is UNDEF (== 0)
281     def yield_to_maturity(quote=false)
282         if not quote
283             return false if @ytm_asked_for_lastquote
284             quote = self.Security.last_quote
285             @ytm_asked_for_lastquote = true if quote.last.zero?
286         end
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)
293         transactions = []
295         # purchase at quote is a cash-outflow
296         transactions << Finance::Transaction.new(
297             denomination * (quote.last/100) * -1 ,
298             :date=>quote.time_last
299         )
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
305             )
306         end
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)
313                else
314                    # annualized
315                    Percentage.new(transactions.xirr(0.1).effective)
316                end
317            rescue
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.
322                false
323         end
324     end
326     ##
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
330     end
333     ##
334     # mandatory method: return collection of Smr::CashflowItem with
335     # future/past/all cashflows created by this SecurityBond.
336     #
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
340     # of stocks.
341     #
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
345     # SMR.
346     #
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
359         @cf_stream.get(
360             nominal_value,
361             :start=>start_date, :end=>end_date,
362             :filter=>(options[:type] || :none),
363             :item_link=>options[:item_link]
364         )
365     end
367     ##
368     # mandatory method: inspect :coupon_interval for happening multiple times a year
369     def has_subannual_payments?
370         coupon_interval < 12
371     end
373     ##
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)
377         prev_coupon = s.last
379         if prev_coupon
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 )
383         else 0 end
384    end
386  protected
388     ##
389     # create Smr::CashflowStream instances from SecurityBond attributes and a
390     # number of result caches
391     def init_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(
400             coupon,
401             time_first_coupon,
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,
411         )
412     end