Skip to content

Commit

Permalink
Got most day counts working
Browse files Browse the repository at this point in the history
  • Loading branch information
brymck committed Oct 3, 2011
1 parent 1263fc3 commit 49ad7b0
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 1 deletion.
2 changes: 1 addition & 1 deletion lib/rupee/calendar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ def last_week?(date)
when 2
# Save February, with twenty-eight days clear
# And twenty-nine each leap year ;)
date.day > (date.year % 4 == 0 && date.year % 100 != 0) ? 22 : 21
date.day > (date.year % 4 == 0 && (date.year % 100 != 0 || date.year % 400 == 0)) ? 22 : 21
end
end

Expand Down
73 changes: 73 additions & 0 deletions lib/rupee/day_count.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,77 @@
require "rupee/mixins/find_instance"

module Rupee
# A class representing a day count convention used to determine cash flow
# and accrual dates for fixed income products
class DayCount
include FindInstance

autoload :THIRTY_360, "rupee/day_count/30_360"
autoload :THIRTY_E_360, "rupee/day_count/30e_360"
autoload :THIRTY_E_PLUS_360, "rupee/day_count/30e+_360"
autoload :ACT_360, "rupee/day_count/act_360"
autoload :ACT_365, "rupee/day_count/act_365"
autoload :ACT_ACT, "rupee/day_count/act_act"

# A description of the day count convention
attr :description
# The formula for determining a day count factor (days divided by years)
attr :block

# Create a new DayCount object
def initialize(description, &block)
@description = description
@block = block
end

def factor(from, to)
block.call from, to
end

class << self
# The number of seconds in a day (a difference of <tt>1</tt> between two
# dates in Ruby indicates a difference of one second)
SECONDS_PER_DAY = 86_400.0

private

def days(from, to)
(to - from) / SECONDS_PER_DAY
end

def days_in_year(date)
if leap_year?(date)
366.0
else
365.0
end
end

def end_of_month?(date)
case date.month
when 9, 4, 6, 11
# Thirty days hath September
# April, June and November
date.day == 30
when 1, 3, 5, 7, 8, 10, 12
# All the rest have thirty-one
date.day == 31
when 2
# Save February, with twenty-eight days clear
# And twenty-nine each leap year ;)
date.day == (date.year % 4 == 0 && (date.year % 100 != 0 || date.year % 400 == 0)) ? 29 : 28
end
end

# Determines whether a date falls during a leap year. Leap years include
# all years divisible by 4, with the exception of years divisible by 100
# but not divisible by 400 (got that?). That is, 2004 is a leap year, as is
# 1904. But while 2000 is a leap year (divisible by 400), 1900 is not
# (divisible by 100 but not 400).
def leap_year?(date) # :doc:
year = date.year
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
end
end
end
end
20 changes: 20 additions & 0 deletions lib/rupee/day_count/30_360.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Rupee
class DayCount
# Standard US 30/360
THIRTY_360 = DayCount.new "30/360, typical pay-fixed convention" do |from, to|
m1 = from.month
m2 = to.month
d1 = from.day
d2 = to.day

if end_of_month?(from)
d1 = 30
d2 = 30 if m1 == 2 && end_of_month?(to) && m2 == 2
end

d2 = 30 if d2 == 31 && d1 == 30

(360 * (to.year - from.year) + 30 * (m2 - m1) + (d2 - d1)) / 360.0
end
end
end
17 changes: 17 additions & 0 deletions lib/rupee/day_count/30e+_360.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Rupee
class DayCount
# 30E+/360
THIRTY_E_PLUS_360 = DayCount.new "30E+/360" do |from, to|
m = to.month - from.month
d1 = from.day
d2 = to.day
d1 = 30 if d1 == 31
if d2 == 31
m += 1
d2 = 1
end

(360 * (to.year - from.year) + 30 * m + (d2 - d1)) / 360.0
end
end
end
14 changes: 14 additions & 0 deletions lib/rupee/day_count/30e_360.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Rupee
class DayCount
# Standard European 30/360
THIRTY_E_360 = DayCount.new "30E/360" do |from, to|
d1 = from.day
d2 = to.day
d1 = 30 if end_of_month?(from)
d2 = 30 if end_of_month?(to) # && (d2 != maturity_date || to.month != 2)

(360 * (to.year - from.year) + 30 * (to.month - from.month) +
(d2 - d1)) / 360.0
end
end
end
8 changes: 8 additions & 0 deletions lib/rupee/day_count/act_360.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module Rupee
class DayCount
# Actual/360
ACT_360 = DayCount.new "Actual/360, typical LIBOR convention" do |from, to|
days(from, to) / 360.0
end
end
end
8 changes: 8 additions & 0 deletions lib/rupee/day_count/act_365.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module Rupee
class DayCount
# Actual/365
ACT_365 = DayCount.new "Actual/365" do |from, to|
days(from, to) / 365.0
end
end
end
10 changes: 10 additions & 0 deletions lib/rupee/day_count/act_act.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Rupee
class DayCount
# Actual/Actual
ACT_ACT = DayCount.new "Actual/actual" do |from, to|
1 - from.yday / days_in_year(from) +
(to.year - from.year - 1) +
to.yday / days_in_year(to)
end
end
end
57 changes: 57 additions & 0 deletions spec/ruby/day_count_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
require File.dirname(__FILE__) + "/../spec_helper"

describe DayCount do
before :each do
@christmas = Time.new(2011, 12, 25)
@next_february = Time.new(2012, 2, 29)
@many_mays = Time.new(2015, 5, 31)
@tolerance = 0.0000001
end

describe "30/360" do
it "should produce a correct day count factor" do
DayCount::THIRTY_360.factor(@christmas, @next_february).should be_within(@tolerance).of 64 / 360.0
DayCount::THIRTY_360.factor(@christmas, @many_mays).should be_within(@tolerance).of 1236 / 360.0
end
end

describe "30E/360" do
it "should produce a correct day count factor" do
DayCount::THIRTY_E_360.factor(@christmas, @next_february).should be_within(@tolerance).of 65 / 360.0
DayCount::THIRTY_E_360.factor(@christmas, @many_mays).should be_within(@tolerance).of 1235 / 360.0
end
end

describe "30E+/360" do
it "should produce a correct day count factor" do
DayCount::THIRTY_E_PLUS_360.factor(@christmas, @next_february).should be_within(@tolerance).of 64 / 360.0
DayCount::THIRTY_E_PLUS_360.factor(@christmas, @many_mays).should be_within(@tolerance).of 1236 / 360.0
end
end

describe "Act/360" do
it "should produce a correct day count factor" do
DayCount::ACT_360.factor(@christmas, @next_february).should be_within(@tolerance).of 66 / 360.0
DayCount::ACT_360.factor(@christmas, @many_mays).should be_within(@tolerance).of 1253 / 360.0
end
end

describe "Act/365" do
it "should produce a correct day count factor" do
DayCount::ACT_365.factor(@christmas, @next_february).should be_within(@tolerance).of 66 / 365.0
DayCount::ACT_365.factor(@christmas, @many_mays).should be_within(@tolerance).of 1253 / 365.0
end
end

describe "Act/Act" do
it "should produce a correct day count factor" do
DayCount::ACT_ACT.factor(@christmas, @next_february).should be_within(@tolerance).of 6 / 365.0 + 60 / 366.0
DayCount::ACT_ACT.factor(@christmas, @many_mays).should be_within(@tolerance).of 887 / 365.0 + 1
end
end

describe "doing leap year calculations" do
it "should be correct within a normal year"
it "should be correct within a leap year"
end
end

0 comments on commit 49ad7b0

Please sign in to comment.