istox 0.1.83 → 0.1.84

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b813654e16bda14094395623351d865c5abf9269245401cb08aab54bc439f06b
4
- data.tar.gz: 9e15ed189e7156eeecbf812f22db93bd9b2840dcf4912b67f013c6f764d07d3d
3
+ metadata.gz: 854004247090c0884aa24fa0c77208e6cad407509e24762291d1c1080e9d387c
4
+ data.tar.gz: e4a725fb85ecc5f84361da376613e2202a1e43bbcec39bbd831c3d1808f8fd5a
5
5
  SHA512:
6
- metadata.gz: ad6369e5493e10b71c517271a851958894ec01eb73ff4cf736ec0085af8093235cd49ed1ddf5788743da05246f7e0a46a919a57f7008c43ece66f2e4b785372f
7
- data.tar.gz: 9a7c003a0ceb7846c25b404618f39727efac4a11f8aa9872f95eac08064b70171dabf44f471111164ae12959a5ef4499df97aea5166cfd0eff9181252de17141
6
+ metadata.gz: b207971e2dc4c7b7239f216c3a9275ba70bed62288b8f010f78058c508fa7041c0c521ae0d276ce2f93a8d29affea1f9ef206032792cee3b26a060b4e03f1f3a
7
+ data.tar.gz: c2f77e3c9af27b0a7327128496960a518b714d9f71876156e1a886ec7b0c1c87c56c45357bf46c50d79943cb5559ddc28eddf3051a99e0af76f16db016208068
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- istox (0.1.82)
4
+ istox (0.1.83)
5
5
  binding_of_caller
6
6
  bunny (>= 2.12.0)
7
7
  graphlient
@@ -44,20 +44,22 @@ module Istox
44
44
  x = 0 if x.blank?
45
45
  y = 0 if y.blank?
46
46
 
47
- x = to_fixed(x)
48
- y = to_fixed(y)
47
+ # x = to_fixed(x)
48
+ # y = to_fixed(y)
49
49
 
50
- return ::BigDecimal.new('1').to_s if x == y
50
+ (::BigDecimal.new(x.to_s) / ::BigDecimal.new(y.to_s)).truncate(18).to_s
51
51
 
52
- fixed1 = ::BigDecimal.new('1e18')
52
+ # return ::BigDecimal.new('1').to_s if x == y
53
53
 
54
- return from_fixed(x.div(y, 100).mult(fixed1, 100)).to_s if x.div(y, 100).modulo(BigDecimal.new(10)).zero? && x.modulo(y).zero?
54
+ # fixed1 = ::BigDecimal.new('1e18')
55
55
 
56
- r_y = fixed1.mult(fixed1, 100).truncate(0).div(y, 100).truncate(0)
56
+ # return from_fixed(x.div(y, 100).mult(fixed1, 100)).to_s if x.div(y, 100).modulo(BigDecimal.new(10)).zero? && x.modulo(y).zero?
57
57
 
58
- result = from_fixed(x.mult(r_y, 100).truncate(0).div(fixed1, 100).truncate(0))
58
+ # r_y = fixed1.mult(fixed1, 100).truncate(0).div(y, 100).truncate(0)
59
59
 
60
- result.truncate(18).to_s
60
+ # result = from_fixed(x.mult(r_y, 100).truncate(0).div(fixed1, 100).truncate(0))
61
+
62
+ # result.truncate(18).to_s
61
63
  end
62
64
 
63
65
  def round_up(x, digits)
@@ -1,93 +1,232 @@
1
- # frozen_string_literal: true
2
1
  require 'date'
3
2
 
4
- # This class is modified for ISTOX use case based on open source Bondie
5
-
6
- # YTM based on price and date:
7
-
8
- # bond = Istox::Quant::Bond.new(coupon: 0.0622, maturity_date: Date.parse('2016-08-22'), coupon_frequency: 4)
9
- # bond.ytm(Date.parse('2015-09-07'), price: 100.8)
10
- # It will not count exact yield, but it will approximate it - you can set maximum approximation error by adding approximation_error parameter to the method.
11
-
12
- # You can also check interest payments dates:
13
-
14
- # bond = Istox::Quant::Bond.new(coupon: 0.0622, maturity_date: Date.parse('2016-08-22'), coupon_frequency: 4)
15
- # bond.interest_payments(Date.parse('2015-09-07'))
16
- # Currently it will not skip weekends or other days without bond quotations, so it can be inaccurate.
17
-
18
- # Also it's possible to check price on which it will generate given yield:
19
-
20
- # bond = Istox::Quant::Bond.new(coupon: 0.0622, maturity_date: Date.parse('2016-08-22'), coupon_frequency: 4)
21
- # bond.price(0.05156, Date.parse('2015-09-07'))
3
+ # https://en.wikipedia.org/wiki/Day_count_convention#30/360_Bond_Basis
4
+ # For 360 basis: 30/360 Bond Basis
5
+ # For 365 basis: Actual/Actual ICMA
22
6
 
7
+ # This class is modified for ISTOX use case based on open source Bondie
23
8
 
24
9
  module Istox
25
10
  module Quant
26
11
  class Bond
27
12
 
28
- DEFAULT_APPROXIMATION_ERROR = 0.0001
13
+ DEFAULT_APPROXIMATION_ERROR = 0.00001
14
+
15
+ def initialize(coupon: nil, maturity_date: nil, years: nil, coupon_frequency: nil, coupon_payment_dates: nil, face_value: 100, days_of_year: 365)
16
+ raise "Invalid coupon #{coupon}" if (coupon.nil? || !is_number?(coupon))
17
+ raise "Invalid maturity_date #{maturity_date}" if (maturity_date.nil? || maturity_date.methods.include?("strftime"))
18
+ raise "Invalid years #{years}" if (years.nil? || !years.is_a?(Integer))
19
+ raise "Invalid coupon_frequency #{days_of_year}" if (coupon_frequency.nil? || !is_number?(coupon))
20
+ raise "Invalid coupon_payment_dates #{coupon_payment_dates}" if (coupon_payment_dates.nil? || coupon_payment_dates.count == 0 || coupon_payment_dates.any? { |date| date > maturity_date })
21
+ raise "Invalid days_of_year #{days_of_year}" if (days_of_year != 365 && days_of_year != 360)
29
22
 
30
- def initialize(coupon: nil, maturity_date: nil, coupon_frequency: nil, face_value: 100, days_of_year: 365)
31
23
  @coupon = coupon
32
24
  @maturity_date = maturity_date
25
+ @years = years
33
26
  @coupon_frequency = coupon_frequency
34
27
  @days_of_year = days_of_year
35
28
  @face_value = face_value
29
+ @coupon_payment_dates = coupon_payment_dates.sort
30
+ @start_date = (@maturity_date-@years.years)
36
31
  end
37
32
 
38
- def price(ytm, date, fees: 0)
39
- price_for_irr(irr_from_ytm(ytm), date, fees: fees)
33
+ def price(ytm, date, ex_coupon_date: nil, fees: 0)
34
+ # price_for_irr(irr_from_ytm(ytm), date, fees: fees)
35
+ price = price_for_irr(ytm/@coupon_frequency, date, ex_coupon_date: ex_coupon_date, fees: fees)
36
+ price.round(2)
40
37
  end
41
38
 
42
- def interest_payments(from_date)
39
+ # def interest_payments(from_date)
40
+ # payments = []
41
+ # date = @maturity_date
42
+ # while date >= from_date
43
+ # payments << date
44
+ # date = date.prev_month(12/@coupon_frequency)
45
+ # end
46
+ # payments.sort
47
+ # end
48
+
49
+ def coupon_payments(from_date)
43
50
  payments = []
44
- date = @maturity_date
45
- while date >= from_date
46
- payments << date
47
- date = date.prev_month(12/@coupon_frequency)
51
+
52
+ @coupon_payment_dates.each do |payment_date|
53
+ if payment_date > from_date
54
+ payments << payment_date
55
+ end
48
56
  end
57
+
49
58
  payments.sort
50
59
  end
51
60
 
52
- def ytm(date, price: 100, fees: 0, approximation_error: DEFAULT_APPROXIMATION_ERROR)
53
- ytm_down, ytm_up = ytm_limits(price, date, fees: fees)
54
- approximate_ytm(ytm_down, ytm_up, price, date, fees: fees, approximation_error: approximation_error)
61
+ def ytm(date, ex_coupon_date: nil, price: 100, fees: 0, approximation_error: DEFAULT_APPROXIMATION_ERROR)
62
+ ytm_down, ytm_up = ytm_limits(price, date, ex_coupon_date: ex_coupon_date, fees: fees)
63
+ approximate_ytm(ytm_down, ytm_up, price, date, ex_coupon_date: ex_coupon_date, fees: fees, approximation_error: approximation_error)
55
64
  end
56
65
 
57
66
 
58
67
  private
59
68
 
60
- def irr_from_ytm(ytm)
61
- (ytm/@coupon_frequency.to_f + 1) ** @coupon_frequency - 1
69
+ def is_365?
70
+ @days_of_year == 365
71
+ end
72
+
73
+ def add_month(mydate, n)
74
+ if mydate.month != mydate.next_day.month
75
+ # month end
76
+ (mydate.next_day + n.month).prev_day
77
+ else
78
+ mydate.next_day + n.month
79
+ end
62
80
  end
63
81
 
82
+ def is_month_end?(mydate)
83
+ mydate.month != mydate.next_day.month
84
+ end
85
+
86
+ def is_number?(val)
87
+ val.is_a?(Integer) || val.is_a?(Float) || val.is_a?(BigDecimal)
88
+ end
89
+
90
+ def irr_from_ytm(ytm)
91
+ (ytm/@coupon_frequency.to_d + 1) ** @coupon_frequency - 1
92
+ end
64
93
 
65
- def approximate_ytm(ytm_down, ytm_up, price, date, fees: 0, approximation_error: DEFAULT_APPROXIMATION_ERROR)
94
+ def approximate_ytm(ytm_down, ytm_up, price, date, ex_coupon_date: nil, fees: 0, approximation_error: DEFAULT_APPROXIMATION_ERROR)
66
95
  approx_ytm = (ytm_up + ytm_down) / 2.0
67
96
  return approx_ytm if ((ytm_up - approx_ytm)/approx_ytm).abs <= approximation_error
68
- p = price(approx_ytm, date, fees: fees)
97
+ p = price(approx_ytm, date, ex_coupon_date: ex_coupon_date, fees: fees)
69
98
  ytm_down, ytm_up = p < price ? [ytm_down, approx_ytm] : [approx_ytm, ytm_up]
70
- approximate_ytm(ytm_down, ytm_up, price, date, fees: fees, approximation_error: approximation_error)
99
+ approximate_ytm(ytm_down, ytm_up, price, date, ex_coupon_date: ex_coupon_date, fees: fees, approximation_error: approximation_error)
71
100
  end
72
101
 
73
- def ytm_limits(price, date, fees: 0)
102
+ def ytm_limits(price, date, ex_coupon_date: nil, fees: 0)
74
103
  ytm_up = 0.1
75
- ytm_up *= 10 while price(ytm_up, date, fees: fees) > price
104
+ ytm_up *= 10 while price(ytm_up, date, ex_coupon_date: ex_coupon_date,fees: fees) > price
76
105
  # IRR will be never lower than -1, so YTM should be never lower than -coupon_frequency (see irr_from_ytm)
77
- ytm_down = price(0, date, fees: fees) > price ? 0.0 : -@coupon_frequency.to_f
106
+ ytm_down = price(0, date, ex_coupon_date: ex_coupon_date, fees: fees) > price ? 0.0 : -@coupon_frequency.to_d
78
107
  [ytm_down, ytm_up]
79
108
  end
80
109
 
81
- def price_for_irr(irr, date, fees: 0)
110
+ # def price_for_irr(irr, date, fees: 0)
111
+ # raise "Date is after maturity_date!" if date > @maturity_date
112
+ # last = date
113
+ # interest_payments(date).map do |payday|
114
+ # interest = @coupon * @face_value * ((payday-last)/@days_of_year)
115
+ # interest += @face_value if payday == @maturity_date
116
+ # last = payday
117
+ # interest / ((1+irr) ** ((payday-date)/@days_of_year))
118
+ # end.inject(:+) / (1+fees)
119
+ # end
120
+
121
+ def price_for_irr(irr, date, ex_coupon_date: nil, fees: 0)
82
122
  raise "Date is after maturity_date!" if date > @maturity_date
83
- last = date
84
- interest_payments(date).map do |payday|
85
- interest = @coupon * @face_value * ((payday-last)/@days_of_year)
86
- interest += @face_value if payday == @maturity_date
87
- last = payday
88
- interest / ((1+irr) ** ((payday-date)/@days_of_year))
89
- end.inject(:+) / (1+fees)
123
+ if date <= @start_date
124
+ date = @start_date
125
+ end
126
+ last_coupon_payday = @coupon_payment_dates.last
127
+ payment_dates = coupon_payments(date)
128
+
129
+ if payment_dates.count == 0
130
+ # no more coupon payments, we do only 1 discount of accrued interest and face value to the current date
131
+ if is_365?
132
+ discount_factor = 1/(1+irr*(@maturity_date-date)/accrued_interest_discount_days)
133
+ else
134
+ discount_factor = 1/(1+irr*@coupon_frequency*day_count_factor(date, @maturity_date, nil))
135
+ end
136
+ discounted_accrued_interest = @coupon*@face_value*accrued_interest_factor*discount_factor
137
+ discounted_face_value = @face_value*discount_factor
138
+ price = discounted_accrued_interest + discounted_face_value
139
+ else
140
+ # we discount face value and coupons and accrued interest (coupon) at maturity (if any)
141
+ # to the first coupon payment left, and then discount the total to current date
142
+ value_at_first_coupon = 0
143
+ if payment_dates.include?(@maturity)
144
+ # last coupon payment is at maturity
145
+ discount_factor = 1/(1+irr)**(payment_dates.count-1)
146
+ value_at_first_coupon = @face_value*discount_factor
147
+ else
148
+ # last coupon is not at maturity, need to add accrued interest (coupon)
149
+ discount_factor = 1/((1+irr)**(payment_dates.count-1))/(1+irr*@coupon_frequency*accrued_interest_factor)
150
+ discounted_accrued_interest = @coupon*@face_value*accrued_interest_factor*discount_factor
151
+ discounted_face_value = @face_value*discount_factor
152
+ value_at_first_coupon = discounted_accrued_interest + discounted_face_value
153
+ end
154
+
155
+ # first coupon can be pro-rata
156
+ value_at_first_coupon += first_coupon(date, ex_coupon_date)
157
+ if payment_dates.count > 1
158
+ for n in 1..payment_dates.count-1 do
159
+ value_at_first_coupon += @coupon*@face_value/@coupon_frequency/(1+irr)**n
160
+ end
161
+ end
162
+
163
+ # discount value at first coupon date to present value
164
+ if is_365?
165
+ price = value_at_first_coupon/(1+irr*(payment_dates.first-date)/first_coupon_discount_days(date))/(1+fees)
166
+ else
167
+ price = value_at_first_coupon/(1+irr*@coupon_frequency*day_count_factor(date, payment_dates.first, nil))/(1+fees)
168
+ end
169
+ end
170
+
171
+ end
172
+
173
+ def first_coupon(date, ex_coupon_date)
174
+ if !ex_coupon_date.nil? && date > ex_coupon_date
175
+ # after ex coupon date, buyer won't receive next coupon
176
+ return 0
177
+ end
178
+
179
+ coupon = @coupon*@face_value/@coupon_frequency
180
+ if date <= @coupon_payment_dates.first
181
+ coupon = @coupon*@face_value*first_coupon_factor
182
+ end
183
+ coupon
184
+ end
185
+
186
+ def accrued_interest_discount_days
187
+ # the days between last coupon payment date and the next coupon payment date if maturity was longer
188
+ last_coupon_payday = @coupon_payment_dates.last
189
+ (add_month(last_coupon_payday, (12/@coupon_frequency).to_i) - last_coupon_payday).to_i
190
+ end
191
+
192
+ def first_coupon_discount_days(date)
193
+ # first coupon date is based on current date
194
+ payment_dates = coupon_payments(date)
195
+ first_coupon_date = payment_dates.first
196
+ (first_coupon_date - add_month(first_coupon_date, -(12/@coupon_frequency).to_i)).to_i
197
+ end
198
+
199
+ def first_coupon_factor
200
+ date1 = @start_date
201
+ date2 = @coupon_payment_dates.first
202
+ date3 = add_month(date2, -(12/@coupon_frequency).to_i)
203
+ if is_365?
204
+ (date2 - date1)/(date2 - date3)/@coupon_frequency
205
+ else
206
+ day_count_factor(date1, date2, date3)
207
+ end
208
+ end
209
+
210
+ def accrued_interest_factor
211
+ date1 = @coupon_payment_dates.last
212
+ date2 = @maturity_date
213
+ date3 = add_month(date1, (12/@coupon_frequency).to_i)
214
+ day_count_factor(date1, date2, date3)
90
215
  end
216
+
217
+ def day_count_factor(date1, date2, date3)
218
+ if is_365?
219
+ (date2 - date1)/(date3 - date1)/@coupon_frequency
220
+ else
221
+ d1 = [date1.day, 30].min
222
+ d2 = date2.day
223
+ if d1 == 30
224
+ d2 = [d2, 30].min
225
+ end
226
+ (360*(date2.year - date1.year) + 30*(date2.month - date1.month) + d2 - d1)/360.to_d
227
+ end
228
+ end
229
+
91
230
  end
92
231
  end
93
232
  end
data/lib/istox/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Istox
2
- VERSION = '0.1.83'.freeze
2
+ VERSION = '0.1.84'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: istox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.83
4
+ version: 0.1.84
5
5
  platform: ruby
6
6
  authors:
7
7
  - Siong Leng