istox 0.1.83 → 0.1.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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