istox 0.1.158.2 → 0.2.3.pre.2

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: 97f303881190d40e32795203028cfc7e21c68a5e34f4240c63f09cacbc116ee5
4
- data.tar.gz: '01196f2df3a75a91f969d04585b0683176d3624e5bb14894be61074025f33fbe'
3
+ metadata.gz: 6d1176bd57c3e5f7c3cf169446b6aec6c51d101fe1db516fa33a492319bcfd1f
4
+ data.tar.gz: e6a8027c9e9f1ab224af5643209bb78165fd3686d384561a3cde9ae441fe40d7
5
5
  SHA512:
6
- metadata.gz: 349210e9975fde52b911856fae0602bd92d55a2350a809bc6e6f8bf59f2fa9d47b43b5a9f88ddc2c0e57b4cea7eb5bc4be90b9a817155cd606b72b37e4bdbf5d
7
- data.tar.gz: b4c3249825bf6eb114ce7d8d40ed3265b422b258722c449991b2eecc2b30e1b6ac98de6035730294f0c328465f4c640dafa7e6b7426a7e2652a256246927f11e
6
+ metadata.gz: 00f2d9c5ed2399e76cab9b940f733e862eae42b79e4de1a7bec5ba301d1240ae95d9487d6a6b68163625da306b69620f2dd926e3ba7a3b67d65a5ad2415a315c
7
+ data.tar.gz: 742bd4f7b1bccc45dbf031704b1476d68fa52fc9ce68bb1d8445345d177911f9033c0d00807f9408a1cbe5b8a3ca080d529ad9b050ef76a9aeb0658682017c1a
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- istox (0.1.157.9)
4
+ istox (0.2.3.pre.2)
5
5
  amazing_print
6
6
  awesome_print
7
7
  aws-sdk-sns (~> 1)
@@ -64,10 +64,10 @@ GEM
64
64
  i18n (>= 0.7, < 2)
65
65
  minitest (~> 5.1)
66
66
  tzinfo (~> 1.1)
67
- amazing_print (1.2.1)
67
+ amazing_print (1.3.0)
68
68
  amq-protocol (2.3.2)
69
69
  arel (9.0.0)
70
- awesome_print (1.8.0)
70
+ awesome_print (1.9.2)
71
71
  aws-eventstream (1.1.0)
72
72
  aws-partitions (1.318.0)
73
73
  aws-sdk-core (3.96.1)
@@ -86,7 +86,7 @@ GEM
86
86
  aws-xray-sdk (0.11.4)
87
87
  aws-sdk-xray (~> 1.4.0)
88
88
  multi_json (~> 1)
89
- binding_of_caller (0.8.0)
89
+ binding_of_caller (1.0.0)
90
90
  debug_inspector (>= 0.0.1)
91
91
  builder (3.2.4)
92
92
  bullet (5.7.6)
@@ -99,8 +99,9 @@ GEM
99
99
  concurrent-ruby (1.1.6)
100
100
  crass (1.0.6)
101
101
  database_cleaner (1.6.2)
102
- debug_inspector (0.0.3)
102
+ debug_inspector (1.1.0)
103
103
  diff-lcs (1.3)
104
+ e2mmap (0.1.0)
104
105
  erubi (1.9.0)
105
106
  exponential-backoff (0.0.4)
106
107
  factory_bot (4.8.2)
@@ -112,34 +113,40 @@ GEM
112
113
  i18n (~> 0.5)
113
114
  fakeredis (0.7.0)
114
115
  redis (>= 3.2, < 5.0)
115
- faraday (1.0.1)
116
+ faraday (1.3.0)
117
+ faraday-net_http (~> 1.0)
116
118
  multipart-post (>= 1.2, < 3)
119
+ ruby2_keywords
120
+ faraday-net_http (1.0.1)
117
121
  faraday_middleware (1.0.0)
118
122
  faraday (~> 1.0)
119
- ffi (1.13.1)
123
+ ffi (1.15.0)
120
124
  globalid (0.4.2)
121
125
  activesupport (>= 4.2.0)
122
- google-protobuf (3.13.0-universal-darwin)
123
- googleapis-common-protos-types (1.0.5)
124
- google-protobuf (~> 3.11)
125
- graphlient (0.4.0)
126
+ google-protobuf (3.15.6-universal-darwin)
127
+ googleapis-common-protos-types (1.0.6)
128
+ google-protobuf (~> 3.14)
129
+ graphlient (0.5.0)
126
130
  faraday (>= 1.0)
127
131
  faraday_middleware
128
132
  graphql-client
129
- graphql (1.11.4)
133
+ graphql (1.12.6)
130
134
  graphql-client (0.16.0)
131
135
  activesupport (>= 3.0)
132
136
  graphql (~> 1.8)
133
- grpc (1.32.0-universal-darwin)
134
- google-protobuf (~> 3.13)
137
+ grpc (1.36.0-universal-darwin)
138
+ google-protobuf (~> 3.14)
135
139
  googleapis-common-protos-types (~> 1.0)
136
- grpc-tools (1.32.0)
137
- gruf (2.8.1)
140
+ grpc-tools (1.36.0)
141
+ gruf (2.9.1)
138
142
  activesupport (> 4)
139
143
  concurrent-ruby (> 1)
144
+ e2mmap (~> 0.1)
140
145
  grpc (~> 1.10)
141
146
  grpc-tools (~> 1.10)
147
+ json (>= 2.3)
142
148
  slop (~> 4.6)
149
+ thwait (~> 0.1)
143
150
  hashie (3.5.7)
144
151
  i18n (0.9.5)
145
152
  concurrent-ruby (~> 1.0)
@@ -150,6 +157,7 @@ GEM
150
157
  grpc-tools (~> 1.10)
151
158
  slop (~> 4.6)
152
159
  jmespath (1.4.0)
160
+ json (2.5.1)
153
161
  listen (3.0.8)
154
162
  rb-fsevent (~> 0.9, >= 0.9.4)
155
163
  rb-inotify (~> 0.9, >= 0.9.7)
@@ -170,11 +178,11 @@ GEM
170
178
  nio4r (2.3.1)
171
179
  nokogiri (1.10.9)
172
180
  mini_portile2 (~> 2.4.0)
173
- oj (3.10.14)
174
- ougai (1.8.5)
181
+ oj (3.11.3)
182
+ ougai (2.0.0)
175
183
  oj (~> 3.10)
176
- paranoia (2.4.2)
177
- activerecord (>= 4.0, < 6.1)
184
+ paranoia (2.4.3)
185
+ activerecord (>= 4.0, < 6.2)
178
186
  pry (0.12.2)
179
187
  coderay (~> 1.1.0)
180
188
  method_source (~> 0.9.0)
@@ -222,7 +230,7 @@ GEM
222
230
  redis-activesupport (5.2.0)
223
231
  activesupport (>= 3, < 7)
224
232
  redis-store (>= 1.3, < 2)
225
- redis-namespace (1.8.0)
233
+ redis-namespace (1.8.1)
226
234
  redis (>= 3.0.4)
227
235
  redis-rack (2.1.3)
228
236
  rack (>= 2.0.8, < 3)
@@ -254,6 +262,7 @@ GEM
254
262
  rspec-mocks (~> 3.8.0)
255
263
  rspec-support (~> 3.8.0)
256
264
  rspec-support (3.8.0)
265
+ ruby2_keywords (0.0.4)
257
266
  slop (4.8.2)
258
267
  sprockets (3.7.2)
259
268
  concurrent-ruby (~> 1.0)
@@ -265,11 +274,13 @@ GEM
265
274
  sqlite3 (1.3.13)
266
275
  thor (0.20.3)
267
276
  thread_safe (0.3.6)
277
+ thwait (0.2.0)
278
+ e2mmap
268
279
  timecop (0.9.1)
269
280
  tzinfo (1.2.6)
270
281
  thread_safe (~> 0.1)
271
282
  uniform_notifier (1.11.0)
272
- vault (0.15.0)
283
+ vault (0.16.0)
273
284
  aws-sigv4
274
285
  websocket-driver (0.7.0)
275
286
  websocket-extensions (>= 0.1.0)
data/lib/istox.rb CHANGED
@@ -45,12 +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
- require 'istox/helpers/integration_test/settings'
53
-
54
48
  require 'istox/models/blockchain_receipt'
55
49
  require 'istox/models/concerns/blockchain_receipt_query'
56
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,7 +148,8 @@ module Istox
146
148
  next unless ce[:email].present?
147
149
 
148
150
  copy_email_data[:email] = ce[:email]
149
- copy_email_data = copy_email_data.merge(ce[:params])
151
+ copy_email_data[:sid] = ce[:sid]
152
+ copy_email_data = copy_email_data.merge(ce[:params]) if ce[:params].present?
150
153
  end
151
154
 
152
155
  copy_email_data.delete(:copy_emails)
@@ -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.2'.freeze
2
+ VERSION = '0.2.3-2'.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.2
4
+ version: 0.2.3.pre.2
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