minor features, layout improvements, many fixes
[smr.git] / gui / test / unit / security_models_test.rb
blobd68e02c24856dc3cdcc0f51d1037423af321cbff
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 'test_helper'
18 class SecurityModelsTest < ActiveSupport::TestCase
20     ##
21     # The Security model offers a number of possible types. Each :type may have
22     # a corresponding table with specific fields describing it very detailed
23     # and a Active::Records model class on top.
24     #
25     # The model class may offer various methods that do useful things with the
26     # additional data. A few such methods should always be implemented so that
27     # Securities can be shown in a unified form.
28     #
29     # This test tries to instantiate the model class(es) and verifies the
30     # existence of mandatory methods.
31     #
32     # These methods are expected to return False on otherwise empty Security /
33     # Security<TYPE> instances.
34     test "mandatory methods consistency" do
35         smr_seed
37         Security.types.each do |t|
38             next if [ :unknown, :cash ].include?(t)
40             assert s = Security.create(:brief=>'test type=%s'% t, :symbol=>'1234%s' % t)
41             assert tm = s.create_type(t)
42             if s.has_type_model? then
43                 Security::MANDATORY_TYPE_METHODS.each do |m|
44                     assert st = s.get_type, 'obtaining typemodel instance failed for Security of type=%s' % s.type
45                     assert st.methods.include?(m), 'method #%s not implemented for Security of type=%s' % [m, s.type]
47                     retval = st.send(m)
48                     if m == :cashflow
49                         assert retval.is_a?(Array), '#cashflow should return Array. Got: %s for model %s.' % [retval.class, st.class]
50                     elsif m == :cashflow_this_year
51                         assert retval, '#cashflow_this_year returned nil on model %s' % st.class
52                         assert retval.to_f >= 0, '#cashflow_this_year should never be negative for %s' % st.class
53                     elsif m == :describe
54                         assert retval.is_a?(String), 'expected #describe to return String for model %s' % st.class
55                     else
56                         assert (not retval or retval==0), 'expected nil, False or 0 for %s.%s. Got: %s' % [st.class, m, retval.inspect]
57                     end
58                 end
59             else
60                 assert false, 'Security created with type=%s does not offer typemodel' % t
61             end
62         end
63     end
65     ##
66     # The #cashflow method of each type should implement some mandatory options.
67     # FIXME: to be implemented - dont know how
68 #    test "mandatory cashflow method implementation consistency" do
69 #       Security.types.each do |t|
70 #           assert s = Security.create(:type=>t.total, :brief=>'test type=%s'% t.date, :symbol=>'1234')
71 # p '==> %s' % s.inspect
72 #       end
73 #    end
75     ##
76     # correctness of cashflow produced by SecurityBond model for open-end + fixed-rate bond
77     test "bond cashflow with quarterly payments" do
78         coupon = 0.04
79         interval = 3  # every three months or four times a year
80         bond = SecurityBond.new(
81             :coupon=>coupon, :coupon_interval=>interval,
82             :time_first_coupon=>Time.now - 1.year,
83             :denomination=>100
84         )
86         bond.cashflow.each do |c|
87             assert c.is_a? Smr::CashflowItem
88             assert c.date.is_a? Time
89             assert c.total.is_a? Float
90             assert_equal coupon * (interval / 12.0), c.total, 'incorrect :coupon amount'
91             assert_equal bond.time_first_coupon.day, c.date.day, 'day of payment not matching day of :time_first_coupon'
92         end
93     end
95     ##
96     # verify yield to maturity (which is an XIRR - date weighted internal rate of return)
97     test "bond yield to maturity" do
98         bond = SecurityBond.new(
99             :coupon=>5.0, :coupon_interval=>12,
100             :time_first_coupon=>Time.new(2001,05,01),
101             :time_last_coupon=>Time.new(2005,05,01),
102             :time_maturity=>Time.new(2005,05,01), :redemption_installments=>1
103         )
105         ytm = bond.yield_to_maturity(Quote.new(:time=>Time.new(2001,05,01), :quote=>100))
106         assert ytm.is_a?(Percentage), 'YTM calculation did not return a Percentage'
107         assert_in_delta Percentage.new(0.06453685).to_f, ytm.to_f, 0.000001, 'YTM calculation incorrect?'
108     end
110     ##
111     # verify yield to maturity for purchases above par value
112     # - this tests a bug in Finance 2.0.0, see https://github.com/bcsgsvn/finance/pull/39
113     # - until a fix is released, apply this patch
114     #     https://github.com/eligoenergy/finance/commit/b8126ede94e5ff79ce431054a9d3533c6bb433ed
115     #   manually to make this test pass
116     test "bond yield to maturity with quote above par" do
117         bond = SecurityBond.new(
118             :coupon=>6.75, :coupon_interval=>12,
119             :time_first_coupon=>Time.new(2012,04,26),
120             :time_last_coupon=>Time.new(2018,04,26),
121             :time_maturity=>Time.new(2018,04,26), :redemption_installments=>1
122         )
124         ytm = bond.yield_to_maturity(Quote.new(:time=>Time.new(2015,12,30), :quote=>105.3))
125         assert ytm.is_a?(Percentage), 'YTM calculation did not return a Percentage'
126         assert ytm.to_f > 0.0, 'positive YTM expected'
127         assert_in_delta Percentage.new(0.06379494).to_f, ytm.to_f, 0.000001, 'YTM calculation incorrect?'
128     end
130     ##
131     # verify yield to maturity when lifetime remaining is below 1
132     # year: it should be absolut, *not* annualized
133     test "bond yield to maturity with redemption below 1 year away" do
134         bond = SecurityBond.new(
135             :coupon=>5.0, :coupon_interval=>12,
136             :time_first_coupon=>Time.now - 1.year + 1.month - 1.day,
137             :time_last_coupon=>Time.now + 1.month - 1.day,
138             :time_maturity=>Time.now + 1.month,
139             :redemption_installments=>1
140         )
142         ytm = bond.yield_to_maturity(Quote.new(:time=>Time.now, :quote=>90))
143         assert ytm.is_a?(Percentage), 'YTM calculation did not return a Percentage'
144         assert_in_delta Percentage.new(0.1666).to_f, ytm.to_f, 0.02, 'YTM calculation incorrect?'
145     end
147     ##
148     # Verify cashflow of open-end bond: no :date_maturity, no :date_last_coupon
149     test "bond with undefined maturity" do
150         bond = SecurityBond.new(
151             :coupon=>3.0, :coupon_interval=>12,
152             :time_first_coupon=>Time.new(2011,06,01),
153         )
155         assert_not bond.cashflow.empty?, 'open-ended bond can not have empty cashflow'
156         assert bond.cashflow.first.is_a? Smr::CashflowItem
158         cf = bond.cashflow(:end_date=>Time.new(2014,05,01))
159         assert cf.last.date <= Time.new(2014,05,01), '#cashflow not respecting given :end_date'
160     end
162     ##
163     # verify yield to (next) call
164     test "bond yield to call" do
165         bond = SecurityBond.new(
166             :coupon=>5.0, :coupon_interval=>12,
167             :time_issue=>Time.new(2000,01,01),
168             :time_first_coupon=>Time.new(2001,01,01),
169             :time_last_coupon=>Time.new(2015,01,01),
170             :time_maturity=>Time.new(2015,01,01), :redemption_installments=>1,
171             :time_first_call=>Time.new(2010,01,01), :call_price=>101.0, :call_interval=>1
172         )
174         q = Quote.new(:time=>Time.new(2000,01,01), :quote=>100)
175         ytc = bond.yield_to_call q
176         ytm = bond.yield_to_maturity q
178         assert ytc.is_a?(Percentage), 'YTC calculation did not return a Percentage'
179         assert ytc > ytm, 'YTC should be greater than YTM'
180         assert_in_delta Percentage.new(0.05074817).to_f, ytc.to_f, 0.000001, 'YTC calculation incorrect?'
181     end
183     ##
184     # correctness of cashflow produced by SecurityStock model
185     test "stock dividend cashflow with annual payments" do
186         dividend = 2.0
187         stock = SecurityStock.new(
188             :dividend=>dividend, :dividend_interval=>12,
189             :time_dividend_exdate=>Time.now,
190         )
192         stock.cashflow.each do |c|
193             assert c.is_a? Smr::CashflowItem
194             assert c.date.is_a? Time
195             assert c.total.is_a? Float
196             assert_equal dividend, c.total, 'incorrect :dividend amount'
197             assert_equal [stock.time_dividend_exdate.day, stock.time_dividend_exdate.month],
198                          [c.date.day, c.date.month],
199                          'day/month of payment not matching day/month of :dividend_exdate'
200         end
201     end
203     test "stock dividend cashflow with quarterly payments" do
204         dividend = 0.625
205         stock = SecurityStock.new(
206             :dividend=>dividend, :dividend_interval=>3,
207             :time_dividend_exdate=>Time.now,
208         )
210         assert_equal 4 * 10, stock.cashflow.count, 'payment count does not match, expected 4 semi-annual payments for 10 years'
211         assert_equal dividend, stock.cashflow.first.total, 'first payment does not match :dividend'
212         assert_equal dividend, stock.cashflow.last.total, 'last payment does not match :dividend'
214         sum_4quarters=0
215         stock.cashflow[0..3].each{|i| sum_4quarters += i.total}
216         assert_equal dividend * 4, sum_4quarters, 'cashflow of 4 quarters not matching 4 times :dividend'
217     end
219     ##
220     # correctness of cashflow produced by the SecurityFund model
221     test "fund cashflow from yearly distributions" do
222         distribution = 3.0
223         fund = SecurityFund.new(
224             :distribution=>distribution, :distribution_interval=>12,
225             :time_distribution=>Time.now,
226         )
228         # try annual payments
229         fund.cashflow.each do |c|
230             assert c.is_a? Smr::CashflowItem
231             assert c.date.is_a? Time
232             assert c.total.is_a? Float
233             assert_equal distribution, c.total, 'incorrect :distribution amount'
234             assert_equal [fund.time_distribution.day, fund.time_distribution.month],
235                          [c.date.day, c.date.month],
236                          'day/month of payment not matching day/month of :date_distribution'
237         end
238    end