ability to settle cashpositions
[smr.git] / gui / app / models / security_bond.rb
blobf699684e57c0c672c0f3b138aab8e231696bae71
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'
20 class SecurityBond < ActiveRecord::Base
21     include Smr::HelperMethods
23     self.inheritance_column = :none
24         has_one :Security, :foreign_key=>:id_security_bond, :inverse_of=>:SecurityBond
26     ##
27     # types of bond supported (may alter behaviour of some methods)
28     # NOTE: do NOT change the relations! just ADD!
29     enum type: { :fixed_rate=>0, :floating_rate=>1, :zero_rate=>2 }
31     # data validations
32 #    validates :date_maturity, :date_first_coupon, :date_last_coupon, numericality: { greater_than_or_equal_to: 0 }
34     ##
35     # Returns human readable String describing :type. Lookup may be given as
36     # symbol, as string or as numerical index.
37     def SecurityBond.describe_type(lookup)
38         descriptions = {
39             :fixed_rate     => 'Fixed Interest Rate',
40             :floating_rate  => 'Floating Interest Rate',
41             :zero_rate      => 'Zero Interest Rate'
42         }
44         if lookup.is_a?(Numeric) then
45             lookup = self.types.key(lookup) || ''
46         end
47         descriptions[lookup.to_sym]
48     end
50     ##
51     # Describes #type of the instance object.
52     def describe_type
53         SecurityBond.describe_type(type)
54     end
56     ##
57     # Returns humanreadable String describing the model itself.
58     def SecurityBond.describe
59         'Debt Ownership'
60     end
61     def describe
62         SecurityBond.describe
63     end
65     ##
66     # interest calculation methods supported
67     enum interest_method: {
68         :act_act=>0, :act_366=>1, :act_365=>2, :act_360=>3,
69         :_30_360=>4, :_30_365=>5, :_30_360=>6
70     }
72     ##
73     # Returns human readable String describing the :interest_method. May be
74     # given as symbol, as string or as numerical index.
75     def SecurityBond.describe_interest_method(lookup)
76         descriptions = {
77             :act_act => 'ICMA Rule: actual days per month and per year.',
78             :act_366 => 'Actual days per month, 366 days per year.',
79             :act_365 => 'English: actual days per month, 365 days per year.',
80             :act_360 => 'European / French: actual days per month, 360 days per year.',
81             :_30_366 => '30 days per month, 366 days per year.',
82             :_30_365 => '30 days per month, 365 days per year.',
83             :_30_360 => 'German: 30 days per month, 360 days per year.',
84         }
86         if lookup.is_a?(Numeric) then
87             lookup = self.interest_methods.key(lookup) || ''
88         end
89         descriptions[lookup.to_sym]
90     end
92     ##
93     # Describes #interest_method of the instance object.
94     def describe_interest_method
95         SecurityBond.describe_interest_method(interest_method)
96     end
98     ##
99     # Human readable String composed of essential properties known by a
100     # SecurityStock on its own. Useful as part to compose the name of a
101     # Security.
102     def to_s
103         s = Array.new
104         s << '%s%%' % coupon if coupon > 0
106         if date_maturity <= 0
107             _redemption_year = 'UNDEF'
108         else
109             if redemption_installments == 1
110                 _redemption_year = time_maturity.strftime('%y')
111             else
112                 _redemption_year = '%s-%s' % [
113                     (time_maturity - ((redemption_installments-1) * redemption_interval).months).year,
114                     time_maturity.strftime('%y')
115                 ]
116             end
117         end
118         s << '%s(%s)' % [time_first_coupon.year, _redemption_year]
120         s << 'FLR' if type == 1
121         s << 'Zero' if type == 2
122         s << currency if not currency.blank? and currency != Smr::DEFAULT_CURRENCY
124         s.join ' '
125     end
127     ##
128     # Wrapper to convert integer date_* column to Time and vice versa.
129     # If :integer is given, the date_* field is updated first. The Time
130     # value is always returned.
131     def time_maturity=(string)
132         write_attribute(:date_maturity, string.to_time)
133     end
134     def time_maturity
135         Time.at read_attribute(:date_maturity)
136     end
138     def time_first_coupon=(string)
139         write_attribute(:date_first_coupon, string.to_time)
140     end
141     def time_first_coupon
142         Time.at read_attribute(:date_first_coupon)
143     end
145     def time_last_coupon=(string)
146         write_attribute(:date_last_coupon, string.to_time)
147     end
148     def time_last_coupon
149         Time.at read_attribute(:date_last_coupon)
150     end
152     ##
153     # mandatory method: result based on :coupons and :quote.
154     def current_yield(quote=false)
155         quote = self.Security.last_quote unless quote
156         raise ':quote must be of Quote' if quote and not quote.is_a?(Quote)
157         if quote.quote.zero? then return false end
158         BigDecimal(coupon.to_s).as_percentage_of(quote.quote)
159     end
161     ##
162     # mandatory method: YTM if all coupons and par is payed as planned until
163     # :date_maturity
164     # False is returned if there is no Quote or :date_maturity is UNDEF (== 0)
165     def yield_to_maturity(quote=false)
166         quote = self.Security.last_quote unless quote
167         raise ':quote must be of Quote' if quote and not quote.is_a?(Quote)
168         return false if quote.quote.zero? or date_maturity <= 0
170         transactions = []
172         # purchase at quote is a cash-outflow
173         transactions << Finance::Transaction.new(
174             denomination * (quote.quote/100) * -1 ,
175             :date=>quote.time
176         )
178         # coupons + redemptions will flow back in
179         cashflow(:start_date=>quote.time).each do |c|
180             transactions << Finance::Transaction.new(
181                 c.total, :date=>c.date
182             )
183         end
185         begin
186             Percentage.new(transactions.xirr.effective)
187         rescue
188             # FIXME: some Security records trigger
189             #     ActionView::Template::Error (Failed to reduce function values.)
190             # at the Percentage.new(...) line. Dont understand what or why?!
191             false
192         end
193     end
195     ##
196     # mandatory method: True if :date is past :date_maturity
197     def is_expired?(date=Time.now)
198         true if date > time_maturity and date_maturity > 0
199     end
201     ##
202     # mandatory method: sums outstanding coupons/redemptions happening before
203     # end of year, returns absolute number.
204     def cashflow_this_year(date=Time.now)
205         cf=0
206         cashflow(:start_date=>date, :end_date=>date.end_of_year).each{|c| cf += c.total}
207         cf
208     end
210     ##
211     # mandatory method: return collection of Smr::CashflowItem with
212     # future/past/all cashflows created by this SecurityBond.
213     #
214     # The :amount option states the amount of Security items for which the
215     # cashflow is calculated. If not given, the smallest possible amount is
216     # used. This is :denomination in the case of bonds, one share in the case
217     # of stocks.
218     #
219     # The :type option allows to filter what is returned. This model supports
220     # :none, :dividend_booking and :redemption_booking. Also see
221     # PositionRevision#types to have all types of cashflow known to
222     # SMR.
223     #
224     # In case :date_maturity is UNDEF (== 0) the cashflow is projected for 10
225     # years from :start_date.
226     def cashflow(options={:start_date=>false, :end_date=>false, :amount=>false, :type=>:none})
227         start_date = options[:start_date] || time_first_coupon
228         end_date = options[:end_date]    if options[:end_date]
229         end_date = time_maturity         if not end_date and date_maturity>0
230         end_date = start_date + 10.years if not end_date
232         last_coupon_payment = (date_last_coupon > 0 ? time_last_coupon : start_date + 10.years)
233         nominal_value = options[:amount] || denomination
234         filter = options[:type] || :none
235         redemptions = Array.new
236         coupons = Array.new
238         # loop redemptions
239         # - backwards, from last to first
240         if date_maturity > 0 and [:none, :redemption_booking].include? filter
241             r = time_maturity
242             while r >= time_maturity - (redemption_installments - 1).years
243                 redemptions << Smr::CashflowItem.new(
244                     r, 'Redemption', nominal_value / redemption_installments,
245                     :type=>:redemption_booking, :comment=>self.Security.to_s
246                 )
247                 r -= 1.year
248             end
249         end
250         redemptions.reverse!
252         if [:none, :dividend_booking].include? filter
253             # figure the next coupon
254             next_coupon = figure_next_interval_occurrence(
255                 time_first_coupon,
256                 last_coupon_payment,
257                 coupon_interval.months,
258                 start_date
259             )
261             # loop coupons
262             # - coupon is divided in payment intervals below one year
263             # - nominal_value is reduced by redemptions which then decreases
264             #   the cashflow from that coupon
265             if next_coupon
266                 c = next_coupon
267                 ri = redemptions.count>=1 ? 1 : 0
268                 r = redemptions[ri]
269                 while c <= end_date and c <= last_coupon_payment
270                     if r and c >= r.date
271                         nominal_value -= r.total
272                         r = redemptions[ ri+=1 ]
273                     end
275                     coupons << Smr::CashflowItem.new(
276                         c, 'Coupon', nominal_value * (coupon / (12 / coupon_interval) / 100),
277                         :type=>:dividend_booking, :comment=>self.Security.to_s
278                     )
279                     c += coupon_interval.months
280                 end
281             end
282         end
284         # drop payments not within start_date and end_date
285         # FIXME: avoid this necessity by design, ie. when implementing caching of the entire stream
286         cf = (coupons + redemptions).delete_if{|p| (not p.date.between?(start_date, end_date)) }
287         cf.sort
288     end
290     ##
291     # tell whether interest payments occur subannually
292     def has_subannual_payments?
293         coupon_interval < 12
294     end