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 class SecurityModelsTest < ActiveSupport::TestCase
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.
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.
29 # This test tries to instantiate the model class(es) and verifies the
30 # existence of mandatory methods.
32 # These methods are expected to return False on otherwise empty Security /
33 # Security<TYPE> instances.
34 test "mandatory methods consistency" do
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]
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
54 assert retval.is_a?(String), 'expected #describe to return String for model %s' % st.class
56 assert (not retval or retval==0), 'expected nil, False or 0 for %s.%s. Got: %s' % [st.class, m, retval.inspect]
60 assert false, 'Security created with type=%s does not offer typemodel' % t
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
76 # correctness of cashflow produced by SecurityBond model for open-end + fixed-rate bond
77 test "bond cashflow with quarterly payments" do
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,
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'
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
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?'
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
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?'
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
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?'
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),
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'
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
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?'
184 # correctness of cashflow produced by SecurityStock model
185 test "stock dividend cashflow with annual payments" do
187 stock = SecurityStock.new(
188 :dividend=>dividend, :dividend_interval=>12,
189 :time_dividend_exdate=>Time.now,
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'
203 test "stock dividend cashflow with quarterly payments" do
205 stock = SecurityStock.new(
206 :dividend=>dividend, :dividend_interval=>3,
207 :time_dividend_exdate=>Time.now,
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'
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'
220 # correctness of cashflow produced by the SecurityFund model
221 test "fund cashflow from yearly distributions" do
223 fund = SecurityFund.new(
224 :distribution=>distribution, :distribution_interval=>12,
225 :time_distribution=>Time.now,
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'