istox 0.1.158.1 → 0.2.3.pre.1

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: 4fd4361212a6baa9b4f3c29c5c5b0bfcef7b5aecf669c29d9ed5ddcfc5d0f446
4
- data.tar.gz: 71cb99709647bb1f9cc867b685c51263b2fd185c383e355461a67b47fe255e3b
3
+ metadata.gz: 5794449030e0819921e5edb6cdc091dc809b00a06b49facb84a296ef31da001a
4
+ data.tar.gz: 357cf1556b389dfc212ad5521f0e674ba58053fa2a4111183574e921ddbc85b4
5
5
  SHA512:
6
- metadata.gz: b488267ae4edf9ef90a6cf6921c7b0d0ca97e5e2f1633475610a27782f6da5fb26ac5ce35b17e45c42fecfc78c883eedbff1b1702ff8d3436314cf8b52bdb115
7
- data.tar.gz: c870f1aaadaa9868e1d17cafe3f83e43f816469bd2ef8af072012df7a0fdf3022b27ca6065e30ce7833c18cb370b7f595c4bcb55e81b367dbb2f957cc48fa695
6
+ metadata.gz: b190f30771d9fac19c7d4df0d4c1386bb8804e12f0cd0ff81c0c29c13be3528d39d2fc23cb05546a12ca7a2ea69a1d40c65ca160e7fd28bad4d41fa4d31ce391
7
+ data.tar.gz: eebf61512d1009bd1fe584ae498fd558724f55ee24b6af868635b7294ba8eaf2c7b06bb3042150f362bee7b1e2487ea8a803dd09bdf702aa44b49fe60c7364fe
data/lib/istox.rb CHANGED
@@ -45,11 +45,6 @@ module Istox
45
45
  require 'istox/helpers/xray/rabbitmq_consumer_interceptor'
46
46
  require 'istox/helpers/xray/xray_initializer'
47
47
 
48
- require 'istox/helpers/integration_test/grpc_client_interceptor'
49
- require 'istox/helpers/integration_test/grpc_server_interceptor'
50
- require 'istox/helpers/integration_test/rabbitmq_publisher_interceptor'
51
- require 'istox/helpers/integration_test/rabbitmq_consumer_interceptor'
52
-
53
48
  require 'istox/models/blockchain_receipt'
54
49
  require 'istox/models/concerns/blockchain_receipt_query'
55
50
  require 'istox/consumers/blockchain_status_handler'
@@ -58,5 +58,11 @@ module Istox
58
58
  obj
59
59
  end
60
60
  end
61
+
62
+ def self.get_currency_decimal(currency)
63
+ return 0 if currency&.downcase == 'jpy'
64
+
65
+ 2
66
+ end
61
67
  end
62
68
  end
@@ -17,8 +17,18 @@ module Istox
17
17
  end
18
18
 
19
19
  # format a money, eg. 20000.134 > SGD 20,000.14, position can be :front or :behind, abs_num: whether to absolute the number
20
- def money(input, round_mode: :half_up, precision: 2, currency:, position: :front, abs_num: false)
21
- result = number(input, round_mode: round_mode, precision: precision, abs_num: abs_num)
20
+ def money(input, round_mode: :half_up, precision: nil, currency:, position: :front, abs_num: false, hide_currency: false)
21
+ # precision = ::Istox::CommonHelper.get_currency_decimal(currency, precision)
22
+
23
+ decimal_place = if precision.present?
24
+ precision
25
+ else
26
+ ::Istox::CommonHelper.get_currency_decimal(currency)
27
+ end
28
+
29
+ result = number(input, round_mode: round_mode, precision: decimal_place, abs_num: abs_num)
30
+
31
+ return result if hide_currency
22
32
 
23
33
  return currency + ' ' + result if position == :front
24
34
 
@@ -93,6 +93,7 @@ module Istox
93
93
  # sample template data, it should be an array
94
94
  # [{
95
95
  # email: email,
96
+ # sid: xxxxx,
96
97
  # istoxP1: auth.first_name,
97
98
  # <more other sample template attributes>: <other sample template data>,
98
99
  # }]
@@ -137,7 +138,8 @@ module Istox
137
138
  else
138
139
  # if it is a object hash
139
140
  # {
140
- # email: xxxx
141
+ # email: xxxx,
142
+ # sid: xxxx,
141
143
  # params: {
142
144
  # istoxP1: xxxx,
143
145
  # istoxP2: xxxx
@@ -146,6 +148,7 @@ module Istox
146
148
  next unless ce[:email].present?
147
149
 
148
150
  copy_email_data[:email] = ce[:email]
151
+ copy_email_data[:sid] = ce[:sid]
149
152
  copy_email_data = copy_email_data.merge(ce[:params])
150
153
  end
151
154
 
@@ -1,7 +1,7 @@
1
1
  module Istox
2
2
  class OrderBookPriceTime
3
3
  class << self
4
- def allocation(soft_cap, total_supply, investments)
4
+ def allocation(soft_cap, total_supply, investments, decimal_place: 2)
5
5
  # sort by token price desc, and id asc
6
6
  investments = investments.sort do |a, b|
7
7
  [b[:token_price], a[:id]] <=> [a[:token_price], b[:id]]
@@ -15,7 +15,7 @@ module Istox
15
15
  total_allocated = 0.0
16
16
  total_unallocated = 0.0
17
17
  total_investment = 0.0
18
- cutoff_price = ::Istox::FMath.round_up(::Istox::FMath.div(soft_cap, total_supply), 2)
18
+ cutoff_price = ::Istox::FMath.round_up(::Istox::FMath.div(soft_cap, total_supply), decimal_place)
19
19
  is_cutoff = false
20
20
 
21
21
  # return result immediately if no interest
@@ -14,7 +14,7 @@ module Istox
14
14
 
15
15
  message = JSON.generate(message) unless message.is_a? String
16
16
 
17
- sns_client.publish({ topic_arn: topic_arn || ENV.fetch('PUSHER_TOPIC_ARN', ''), message: message })
17
+ sns_client.publish(topic_arn: topic_arn || ENV.fetch('SNS_CLIENT_TOPIC', ''), message: message)
18
18
 
19
19
  log.info('Publish to SNS successfully.')
20
20
  rescue StandardError => e
@@ -28,8 +28,15 @@ module Istox
28
28
  def sns_client
29
29
  return @sns_client if @sns_client.present?
30
30
 
31
- Aws.config.update({ region: ENV.fetch('AWS_REGION', 'ap-southeast-1'),
32
- credentials: Aws::Credentials.new(ENV.fetch('AWS_STS_ACCESS_KEY_ID'), ENV.fetch('AWS_STS_SECRET_ACCESS_KEY')) })
31
+ access_key = ENV.fetch('AWS_STS_ACCESS_KEY_ID', nil)
32
+
33
+ if access_key.present?
34
+ credentials = Aws::Credentials.new(ENV.fetch('AWS_STS_ACCESS_KEY_ID', nil), ENV.fetch('AWS_STS_SECRET_ACCESS_KEY', nil))
35
+ Aws.config.update(region: ENV.fetch('AWS_REGION', 'ap-southeast-1'),
36
+ credentials: credentials)
37
+ else
38
+ Aws.config.update(region: ENV.fetch('AWS_REGION', 'ap-southeast-1'))
39
+ end
33
40
 
34
41
  @sns_client = Aws::SNS::Client.new(region: ENV.fetch('AWS_REGION', 'ap-southeast-1'))
35
42
  end
@@ -22,6 +22,7 @@ module Istox
22
22
 
23
23
  request_context.metadata[:xray_trace_id] = XRay.recorder.current_segment.trace_id
24
24
  request_context.metadata[:xray_parent_id] = XRay.recorder.current_segment.id
25
+ Thread.current[:tracer_id] = XRay.recorder.current_segment.trace_id
25
26
 
26
27
  result = yield
27
28
 
@@ -7,6 +7,7 @@ module Istox
7
7
  meta = request.active_call.metadata
8
8
  trace_id = (meta['xray_trace_id'] if meta.present? && meta.key?('xray_trace_id'))
9
9
  parent_id = (meta['xray_parent_id'] if meta.present? && meta.key?('xray_parent_id'))
10
+ Thread.current[:tracer_id] = trace_id
10
11
 
11
12
  XRay.recorder.begin_segment("#{::Istox::Xray::XrayInitializer.service_name}.grpc.#{request.method_key}",
12
13
  trace_id: trace_id, parent_id: parent_id)
@@ -8,6 +8,7 @@ module Istox
8
8
 
9
9
  trace_id = (payload_hash[:xray_trace_id] if payload_hash.key?(:xray_trace_id))
10
10
  parent_id = (payload_hash[:xray_parent_id] if payload_hash.key?(:xray_parent_id))
11
+ Thread.current[:tracer_id] = trace_id
11
12
 
12
13
  arr = [delivery_info[:exchange]&.downcase, delivery_info[:routing_key]&.downcase, payload_hash[:type]&.downcase].compact
13
14
 
@@ -22,6 +22,7 @@ module Istox
22
22
 
23
23
  message[:xray_trace_id] = XRay.recorder.current_segment.trace_id
24
24
  message[:xray_parent_id] = XRay.recorder.current_segment.id
25
+ Thread.current[:tracer_id] = XRay.recorder.current_segment.trace_id
25
26
 
26
27
  XRay.recorder.end_subsegment
27
28
 
@@ -12,45 +12,82 @@ module Istox
12
12
 
13
13
  DEFAULT_APPROXIMATION_ERROR = 0.00001
14
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))
15
+ def initialize(coupon: nil, maturity_date: nil, start_date: 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)) || coupon < 0
17
17
  raise "Invalid maturity_date #{maturity_date}" if (maturity_date.nil? || maturity_date.methods.include?("strftime"))
18
- raise "Invalid years #{years}" if (years.nil? || !is_number?(years))
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.to_date })
18
+ raise "Invalid start_date #{start_date}" if (start_date.nil? || start_date.methods.include?("strftime"))
19
+ raise "Invalid coupon_frequency #{coupon_frequency}" if (coupon_frequency.nil? || !coupon_frequency.is_a?(Integer) || coupon_frequency < 0)
20
+ raise "Invalid coupon_payment_dates #{coupon_payment_dates}" if (coupon_payment_dates.nil? || (coupon_payment_dates.count == 0 && coupon_frequency != 0) || coupon_payment_dates.any? { |date| date > maturity_date.to_date })
21
21
  raise "Invalid days_of_year #{days_of_year}" if (days_of_year != 365 && days_of_year != 360)
22
+ raise "start_date is not before maturity_date" if start_date>=maturity_date
22
23
 
23
24
  @coupon = coupon.to_d
24
25
  @maturity_date = maturity_date.to_date
25
- @years = years
26
- @coupon_frequency = coupon_frequency.to_d
26
+ @coupon_frequency = coupon_frequency.to_i # if this is 0, it means zero coupon
27
27
  @days_of_year = days_of_year.to_d
28
- @face_value = face_value
29
- @coupon_payment_dates = coupon_payment_dates.map(&:to_date).sort
30
- @start_date = (@maturity_date-(years*12).to_i.months)
28
+ @face_value = face_value.to_d
29
+ @coupon_payment_dates = coupon_payment_dates.map(&:to_date).uniq.sort
30
+ # note here we work out the start date based on maturity date and nunber of years
31
+ @start_date = start_date.to_date
32
+
33
+ @pay_accrued_interest = false
34
+ @coupon_payment_dates_include_accrued_interest = false
35
+ if !is_zero_coupon?
36
+ if @coupon_payment_dates.include?(@maturity_date)
37
+ # maturity date is a coupon payment date, check if this should
38
+ # be accrued interest or last normal coupon
39
+ if @coupon_payment_dates.count > 1
40
+ previous_coupon_date = @coupon_payment_dates[@coupon_payment_dates.count-2]
41
+ next_coupon_date = add_month(previous_coupon_date, -(12/@coupon_frequency).to_i)
42
+ # If maturity date is a normal coupon payment, the theorecical next_coupon_date
43
+ # calculated from previous coupon payment should be the maturity date, to be safe,
44
+ # we allow 3 days difference
45
+ if (next_coupon_date - @maturity_date).abs <= 3
46
+ @pay_accrued_interest = false
47
+ @coupon_payment_dates_include_accrued_interest = false
48
+ else
49
+ @pay_accrued_interest = true
50
+ @coupon_payment_dates_include_accrued_interest = true
51
+ end
52
+ else
53
+ # maturity date is only coupon payment date, shouldn't be accrued interest!
54
+ @pay_accrued_interest = false
55
+ @coupon_payment_dates_include_accrued_interest = false
56
+ end
57
+ else
58
+ # maturity date is not included in coupon payment date, consider
59
+ # this needs to pay accrued interest
60
+ @pay_accrued_interest = true
61
+ @coupon_payment_dates_include_accrued_interest = false
62
+ end
63
+ end
64
+
65
+ log.info "Bond info: start_date=#{@start_date} maturity_date=#{@maturity_date} days_of_years=#{days_of_year} coupon=#{@coupon} coupon_frequency=#{coupon_frequency} face_value=#{@face_value} coupon_payment_dates=#{@coupon_payment_dates} pay_accrued_interest=#{@pay_accrued_interest} coupon_payment_dates_include_accrued_interest=#{@coupon_payment_dates_include_accrued_interest}"
31
66
  end
32
67
 
33
68
  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)
69
+ irr = ytm
70
+ irr = ytm/@coupon_frequency if !is_zero_coupon?
71
+ price = price_for_irr(irr, date, ex_coupon_date: ex_coupon_date, fees: fees)
36
72
  price
37
73
  end
38
74
 
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
75
+ def ytm(date, ex_coupon_date: nil, price: 100, fees: 0, approximation_error: DEFAULT_APPROXIMATION_ERROR)
76
+ ytm_down, ytm_up = ytm_limits(price, date, ex_coupon_date: ex_coupon_date, fees: fees)
77
+ approximate_ytm(ytm_down, ytm_up, price, date, ex_coupon_date: ex_coupon_date, fees: fees, approximation_error: approximation_error)
78
+ end
79
+
80
+
81
+ private
48
82
 
49
83
  def coupon_payments(from_date)
50
84
  payments = []
51
85
 
52
86
  @coupon_payment_dates.each do |payment_date|
53
- if payment_date > from_date
87
+ # if from_date falls on coupon payment date or maturity date, we
88
+ # still add them to make it easy for calculation later
89
+ # just that the first coupon will not pay actually (i.e. 0)
90
+ if payment_date >= from_date && payment_date <= @maturity_date
54
91
  payments << payment_date
55
92
  end
56
93
  end
@@ -58,29 +95,25 @@ module Istox
58
95
  payments.sort
59
96
  end
60
97
 
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)
64
- end
65
-
66
-
67
- private
68
-
69
98
  def is_365?
70
99
  @days_of_year == 365.to_d
71
100
  end
72
101
 
73
- def add_month(mydate, n)
74
- if mydate.month != mydate.next_day.month
102
+ def is_zero_coupon?
103
+ @coupon == 0
104
+ end
105
+
106
+ def add_month(my_date, n)
107
+ if is_month_end?(my_date)
75
108
  # month end
76
- (mydate.next_day + n.month).prev_day
109
+ (my_date.next_day + n.month).prev_day
77
110
  else
78
- mydate.next_day + n.month
111
+ my_date.next_day + n.month
79
112
  end
80
113
  end
81
114
 
82
- def is_month_end?(mydate)
83
- mydate.month != mydate.next_day.month
115
+ def is_month_end?(my_date)
116
+ my_date.month != my_date.next_day.month
84
117
  end
85
118
 
86
119
  def is_number?(val)
@@ -107,111 +140,158 @@ module Istox
107
140
  [ytm_down, ytm_up]
108
141
  end
109
142
 
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
143
  def price_for_irr(irr, date, ex_coupon_date: nil, fees: 0)
122
144
  date = date.to_date
123
145
  raise "Date is after maturity_date!" if date > @maturity_date
146
+ # if today is maturity, price is face value (without including any coupon/accrued interest)
147
+ # BizOps said we will always have an ex_coupon_date when it's close
148
+ # to coupon payment date but not too early, so the fomula needs to
149
+ # accept nil value. Even if we have ex_coupon_date as maturity date,
150
+ # buyer is not eligible to receive the coupon, so no need to check ex_coupon_date here
151
+ if date == @maturity_date
152
+ return @face_value
153
+ end
154
+
124
155
  if date <= @start_date
125
156
  date = @start_date
126
157
  end
127
- last_coupon_payday = @coupon_payment_dates.last
128
- payment_dates = coupon_payments(date)
129
158
 
130
- if payment_dates.count == 0
131
- # no more coupon payments, we do only 1 discount of accrued interest and face value to the current date
159
+ # it's supporting 365 actual days only at the moment
160
+ if is_zero_coupon?
161
+ total_days = @maturity_date - date
162
+ no_of_years = total_days / 365
163
+ remainder_days = total_days % 365
164
+
165
+ return @face_value/(1+irr)**no_of_years/(1+irr*remainder_days/365)
166
+ end
167
+
168
+ payment_dates = coupon_payments(date)
169
+
170
+ no_regular_coupon_before_maturity = false
171
+ if payment_dates.count == 0 || (payment_dates.count == 1 && payment_dates.first == @maturity_date)
172
+ no_regular_coupon_before_maturity = true
173
+ end
174
+
175
+ if no_regular_coupon_before_maturity
176
+ discount_factor = nil
132
177
  if is_365?
133
- discount_factor = 1.0/(1+irr*(@maturity_date-date)/accrued_interest_discount_days)
178
+ previous_coupon_date = previous_coupon_date_before_maturity
179
+ next_coupon_date = add_month(previous_coupon_date, (12/@coupon_frequency).to_i)
180
+ discount_factor = 1.0/(1+irr*(@maturity_date-date)/(next_coupon_date-previous_coupon_date))
134
181
  else
135
182
  discount_factor = 1.0/(1+irr*@coupon_frequency*day_count_factor(date, @maturity_date, nil))
136
183
  end
137
- discounted_accrued_interest = @coupon*@face_value*accrued_interest_factor*discount_factor
138
- discounted_face_value = @face_value*discount_factor
139
- price = discounted_accrued_interest + discounted_face_value
184
+
185
+ if @pay_accrued_interest
186
+ # accrued interest + face value discounted to the current date
187
+ discounted_accrued_interest = 0
188
+ if ex_coupon_date.nil? || ex_coupon_date == @maturity_date
189
+ discounted_accrued_interest = @coupon*@face_value*accrued_interest_factor*discount_factor
190
+ end
191
+ discounted_face_value = @face_value*discount_factor
192
+ price = discounted_accrued_interest + discounted_face_value
193
+ return price
194
+ else
195
+ discounted_last_coupon = 0
196
+ if ex_coupon_date.nil? || ex_coupon_date == @maturity_date
197
+ discounted_last_coupon = @coupon*@face_value/@coupon_frequency*discount_factor
198
+ end
199
+ discounted_face_value = @face_value*discount_factor
200
+ price = discounted_last_coupon + discounted_face_value
201
+ return price
202
+ end
140
203
  else
141
- # we discount face value and coupons and accrued interest (coupon) at maturity (if any)
142
- # to the first coupon payment left, and then discount the total to current date
204
+ # there are at least 1 more regular coupon payment before maturity
205
+ # we discount face value and coupons or accrued interest if any
206
+ # to the first coupon payment date left, and then discount the total to current date
143
207
  value_at_first_coupon = 0.to_d
144
- if payment_dates.include?(@maturity)
145
- # last coupon payment is at maturity
146
- discount_factor = 1.0/(1+irr)**(payment_dates.count-1)
147
- value_at_first_coupon = @face_value*discount_factor
148
- else
149
- period = @coupon_payment_dates.include?(date) ? payment_dates.count : payment_dates.count-1
150
- # last coupon is not at maturity, need to add accrued interest (coupon)
151
- discount_factor = 1.0/((1+irr)**period)/(1+irr*@coupon_frequency*accrued_interest_factor)
208
+ # this is the discount period excluding the one at maturity date
209
+ # e.g. payment_dates has date1, date2, maturity, we need to discount
210
+ # to date1, excluding the period from date2 to maturity, period = 1 (i.e. from date2 to date1)
211
+ no_of_discount_period = 0
212
+ if @pay_accrued_interest
213
+ if @coupon_payment_dates_include_accrued_interest
214
+ no_of_discount_period = payment_dates.count-2
215
+ else
216
+ no_of_discount_period = payment_dates.count-1
217
+ end
218
+
219
+ discount_factor = 1.0/((1+irr)**no_of_discount_period)/(1+irr*@coupon_frequency*accrued_interest_factor)
152
220
  discounted_accrued_interest = @coupon*@face_value*accrued_interest_factor*discount_factor
153
221
  discounted_face_value = @face_value*discount_factor
154
222
  value_at_first_coupon = discounted_accrued_interest + discounted_face_value
155
- end
223
+ else
224
+ # last coupon payment is at maturity
225
+ no_of_discount_period = payment_dates.count-1
226
+ discount_factor = 1.0/(1+irr)**(no_of_discount_period+1)
227
+ discounted_last_coupon = @coupon*@face_value/@coupon_frequency*discount_factor
228
+ discounted_face_value = @face_value*discount_factor
229
+ value_at_first_coupon = discounted_last_coupon + discounted_face_value
230
+ end
156
231
 
157
- # first coupon can be pro-rata
158
- value_at_first_coupon += first_coupon(date, ex_coupon_date)
159
- if payment_dates.count >= 1
160
- if @coupon_payment_dates.include?(date)
161
- # today is on one of the coupon payment date, the coupon will not be payed today
162
- # discount all coupon payments to first coupon date
163
- for n in 1..payment_dates.count do
164
- value_at_first_coupon += @coupon*@face_value/@coupon_frequency/(1+irr)**n
165
- end
166
- else
167
- # discount all coupon payments excluding first one to first coupon date
168
- for n in 1..payment_dates.count-1 do
169
- value_at_first_coupon += @coupon*@face_value/@coupon_frequency/(1+irr)**n
170
- end
171
- end
232
+ if no_of_discount_period >= 1
233
+ # discount all coupon payments excluding first one to first coupon date
234
+ for n in 1..no_of_discount_period do
235
+ value_at_first_coupon += @coupon*@face_value/@coupon_frequency/(1+irr)**n
236
+ end
172
237
  end
173
238
 
239
+ # first coupon, can be pro-rated, check the logic in first_coupon
240
+ value_at_first_coupon += first_coupon(date, ex_coupon_date)
241
+
174
242
  if @coupon_payment_dates.include?(date)
175
243
  # today is one of the coupon payment, no need to discount
176
244
  price = value_at_first_coupon
177
245
  else
178
246
  # discount value at first coupon date to present value
179
247
  if is_365?
180
- price = value_at_first_coupon/(1+irr*(payment_dates.first-date)/first_coupon_discount_days(date))/(1+fees)
248
+ previous_coupon_date = add_month(payment_dates.first, -(12/@coupon_frequency).to_i)
249
+ price = value_at_first_coupon/(1+irr*(payment_dates.first-date)/(payment_dates.first-previous_coupon_date))/(1+fees)
181
250
  else
182
251
  price = value_at_first_coupon/(1+irr*@coupon_frequency*day_count_factor(date, payment_dates.first, nil))/(1+fees)
183
252
  end
184
253
  end
254
+ return price
185
255
  end
186
-
187
256
  end
188
257
 
189
258
  def first_coupon(date, ex_coupon_date)
190
- if @coupon_payment_dates.include?(date) || (!ex_coupon_date.nil? && date > ex_coupon_date.to_date)
191
- # if today is on coupon payment date, we won't include the coupon
192
- # as by default it's passing ex_coupon_date in our calculation
193
- # after ex coupon date, buyer won't receive next coupon
259
+ # BizOps said we will always have an ex_coupon_date when it's close
260
+ # to coupon payment date but not too early, so the fomula needs to
261
+ # accept nil value.
262
+ if !ex_coupon_date.nil? && date >= ex_coupon_date.to_date
263
+ # on or after ex coupon date, buyer won't receive next coupon
264
+ return 0.to_d
265
+ elsif @coupon_payment_dates.include?(date)
266
+ # if today is a coupon payment date, for sure it will pass ex_coupon_date
267
+ # and buyer won't receive this coupon. In case ex_coupon_date is nil,
268
+ # that's the same assumption, so first_coupon will be 0
194
269
  return 0.to_d
195
270
  end
196
271
 
197
272
  coupon = @coupon*@face_value/@coupon_frequency
198
- if date <= @coupon_payment_dates.first
273
+ # For the very first coupon, we need to check if it needs to be pro-rated
274
+ if date <= @coupon_payment_dates.first && @pay_accrued_interest
199
275
  coupon = @coupon*@face_value*first_coupon_factor
200
276
  end
201
277
  coupon
202
278
  end
203
279
 
204
- def accrued_interest_discount_days
205
- # the days between last coupon payment date and the next coupon payment date if maturity was longer
206
- last_coupon_payday = @coupon_payment_dates.last
207
- (add_month(last_coupon_payday, (12/@coupon_frequency).to_i) - last_coupon_payday).to_i
208
- end
209
-
210
- def first_coupon_discount_days(date)
211
- # first coupon date is based on current date
212
- payment_dates = coupon_payments(date)
213
- first_coupon_date = payment_dates.first
214
- (first_coupon_date - add_month(first_coupon_date, -(12/@coupon_frequency).to_i)).to_i
280
+ def previous_coupon_date_before_maturity
281
+ previous_coupon_date = @start_date
282
+ if @coupon_payment_dates.include?(@maturity_date)
283
+ # maturity date is in coupon payment dates, get the previous coupon date
284
+ # if there is no more date, use the default start date
285
+ if @coupon_payment_dates.count > 1
286
+ previous_coupon_date = @coupon_payment_dates[@coupon_payment_dates.count-2]
287
+ end
288
+ else
289
+ # maturity date is not in coupon payment dates, get the last coupon date
290
+ # if there is no more date, use the default start date
291
+ if @coupon_payment_dates.count > 0
292
+ previous_coupon_date = @coupon_payment_dates.last
293
+ end
294
+ end
215
295
  end
216
296
 
217
297
  def first_coupon_factor
@@ -227,6 +307,13 @@ module Istox
227
307
 
228
308
  def accrued_interest_factor
229
309
  date1 = @coupon_payment_dates.last
310
+ if @coupon_payment_dates_include_accrued_interest
311
+ if @coupon_payment_dates.count > 1
312
+ date1 = @coupon_payment_dates[@coupon_payment_dates.count-2]
313
+ else
314
+ raise "coupon_payment_dates count < 2 and we have accrued interest payment date included!!!"
315
+ end
316
+ end
230
317
  date2 = @maturity_date
231
318
  date3 = add_month(date1, (12/@coupon_frequency).to_i)
232
319
  day_count_factor(date1, date2, date3)
@@ -234,8 +321,10 @@ module Istox
234
321
 
235
322
  def day_count_factor(date1, date2, date3)
236
323
  if is_365?
324
+ # Actual/365
237
325
  (date2 - date1)/(date3 - date1)/@coupon_frequency
238
326
  else
327
+ # 30/360
239
328
  d1 = [date1.day, 30].min
240
329
  d2 = date2.day
241
330
  if d1 == 30
data/lib/istox/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Istox
2
- VERSION = '0.1.158.1'.freeze
2
+ VERSION = '0.2.3-1'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: istox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.158.1
4
+ version: 0.2.3.pre.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Siong Leng
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-12-18 00:00:00.000000000 Z
11
+ date: 2021-03-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: amazing_print
@@ -480,14 +480,6 @@ extensions: []
480
480
  extra_rdoc_files: []
481
481
  files:
482
482
  - ".gitignore"
483
- - ".idea/.rakeTasks"
484
- - ".idea/encodings.xml"
485
- - ".idea/inspectionProfiles/Project_Default.xml"
486
- - ".idea/istox-gem.iml"
487
- - ".idea/misc.xml"
488
- - ".idea/modules.xml"
489
- - ".idea/vcs.xml"
490
- - ".idea/workspace.xml"
491
483
  - ".rubocop.yml"
492
484
  - ".solargraph.yml"
493
485
  - CODE_OF_CONDUCT.md
@@ -511,11 +503,6 @@ files:
511
503
  - lib/istox/helpers/graphql_client.rb
512
504
  - lib/istox/helpers/grpc_client.rb
513
505
  - lib/istox/helpers/gruf_listener_hook.rb
514
- - lib/istox/helpers/integration_test/grpc_client_interceptor.rb
515
- - lib/istox/helpers/integration_test/grpc_server_interceptor.rb
516
- - lib/istox/helpers/integration_test/rabbitmq_consumer_interceptor.rb
517
- - lib/istox/helpers/integration_test/rabbitmq_publisher_interceptor.rb
518
- - lib/istox/helpers/integration_test/settings.rb
519
506
  - lib/istox/helpers/logger.rb
520
507
  - lib/istox/helpers/messaging.rb
521
508
  - lib/istox/helpers/my_open_struct.rb
@@ -555,9 +542,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
555
542
  version: '0'
556
543
  required_rubygems_version: !ruby/object:Gem::Requirement
557
544
  requirements:
558
- - - ">="
545
+ - - ">"
559
546
  - !ruby/object:Gem::Version
560
- version: '0'
547
+ version: 1.3.1
561
548
  requirements: []
562
549
  rubygems_version: 3.0.6
563
550
  signing_key:
data/.idea/.rakeTasks DELETED
@@ -1,7 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <Settings><!--This file was automatically generated by Ruby plugin.
3
- You are allowed to:
4
- 1. Remove rake task
5
- 2. Add existing rake tasks
6
- To add existing rake tasks automatically delete this file and reload the project.
7
- --><RakeGroup description="" fullCmd="" taksId="rake" /></Settings>
data/.idea/encodings.xml DELETED
@@ -1,4 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="Encoding" addBOMForNewFiles="with NO BOM" />
4
- </project>
@@ -1,6 +0,0 @@
1
- <component name="InspectionProjectProfileManager">
2
- <profile version="1.0">
3
- <option name="myName" value="Project Default" />
4
- <inspection_tool class="Rubocop" enabled="false" level="WARNING" enabled_by_default="false" />
5
- </profile>
6
- </component>
data/.idea/istox-gem.iml DELETED
@@ -1,12 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <module type="RUBY_MODULE" version="4">
3
- <component name="ModuleRunConfigurationManager">
4
- <shared />
5
- </component>
6
- <component name="NewModuleRootManager">
7
- <content url="file://$MODULE_DIR$" />
8
- <orderEntry type="inheritedJdk" />
9
- <orderEntry type="sourceFolder" forTests="false" />
10
- <orderEntry type="library" scope="PROVIDED" name="bundler (v1.16.6, RVM: ruby-2.5.3 [global]) [gem]" level="application" />
11
- </component>
12
- </module>
data/.idea/misc.xml DELETED
@@ -1,7 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="JavaScriptSettings">
4
- <option name="languageLevel" value="ES6" />
5
- </component>
6
- <component name="ProjectRootManager" version="2" project-jdk-name="RVM: ruby-2.5.3 [global]" project-jdk-type="RUBY_SDK" />
7
- </project>
data/.idea/modules.xml DELETED
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectModuleManager">
4
- <modules>
5
- <module fileurl="file://$PROJECT_DIR$/.idea/istox-gem.iml" filepath="$PROJECT_DIR$/.idea/istox-gem.iml" />
6
- </modules>
7
- </component>
8
- </project>
data/.idea/vcs.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
- </component>
6
- </project>
@@ -1,22 +0,0 @@
1
- require 'istox/helpers/logger'
2
-
3
- module Istox
4
- module IntegrationTest
5
- class GrpcClientInterceptor < ::Gruf::Interceptors::ClientInterceptor
6
- def call(request_context:)
7
- request_context.metadata[:integration_test] = if ::Istox::IntegrationTest::Settings.enable
8
- '1'
9
- else
10
- '0'
11
- end
12
-
13
- result = yield
14
-
15
- result
16
- rescue StandardError => e
17
- log.error e
18
- raise e
19
- end
20
- end
21
- end
22
- end
@@ -1,31 +0,0 @@
1
- require 'istox/helpers/logger'
2
-
3
- module Istox
4
- module IntegrationTest
5
- class GrpcServerInterceptor < ::Gruf::Interceptors::ServerInterceptor
6
- def call
7
- meta = request.active_call.metadata
8
- enable_integration = (meta['integration_test'] if meta.present? && meta.key?('integration_test'))
9
-
10
- if enable_integration == '1'
11
- ::Istox::IntegrationTest::Settings.enable = true
12
- if ActiveRecord::Base.connection.current_database != Rails.configuration.database_configuration['integration']['database']
13
- ActiveRecord::Base.establish_connection(:integration)
14
- end
15
- else
16
- ::Istox::IntegrationTest::Settings.enable = false
17
- if ActiveRecord::Base.connection.current_database != Rails.configuration.database_configuration['development']['database']
18
- ActiveRecord::Base.establish_connection(:development)
19
- end
20
- end
21
-
22
- result = yield # this returns the protobuf message
23
-
24
- result
25
- rescue StandardError => e
26
- log.error e
27
- raise e
28
- end
29
- end
30
- end
31
- end
@@ -1,32 +0,0 @@
1
- require 'istox/helpers/logger'
2
-
3
- module Istox
4
- module IntegrationTest
5
- class RabbitmqConsumerInterceptor
6
- def call(payload, _metadata, _delivery_info)
7
- payload_hash = payload.to_h
8
-
9
- enable_integration_test = (payload_hash[:istox_integration_test] if payload_hash.key?(:istox_integration_test))
10
-
11
- if enable_integration_test == '1'
12
- ::Istox::IntegrationTest::Settings.enable = true
13
- if ActiveRecord::Base.connection.current_database != Rails.configuration.database_configuration['integration']['database']
14
- ActiveRecord::Base.establish_connection(:integration)
15
- end
16
- else
17
- ::Istox::IntegrationTest::Settings.enable = false
18
- if ActiveRecord::Base.connection.current_database != Rails.configuration.database_configuration['development']['database']
19
- ActiveRecord::Base.establish_connection(:development)
20
- end
21
- end
22
-
23
- result = yield # this returns consumer handler message
24
-
25
- result
26
- rescue StandardError => e
27
- log.error e
28
- raise e
29
- end
30
- end
31
- end
32
- end
@@ -1,14 +0,0 @@
1
- require 'istox/helpers/logger'
2
-
3
- module Istox
4
- module IntegrationTest
5
- class RabbitmqPublisherInterceptor
6
- def call(message, _options)
7
- message[:istox_integration_test] = '1'
8
- rescue StandardError => e
9
- log.error e
10
- raise e
11
- end
12
- end
13
- end
14
- end
@@ -1,11 +0,0 @@
1
- require 'istox/helpers/logger'
2
-
3
- module Istox
4
- module IntegrationTest
5
- class Settings
6
- class << self
7
- attr_accessor :enable
8
- end
9
- end
10
- end
11
- end