istox 0.2.0 → 0.2.3.pre.50

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: 32b60e5cea8c89edc2c9d9a9b4a9aaefeb026351d68ed6898b578f04213f7e81
4
- data.tar.gz: e969dc0c25653d64d0de4173bd59178e3f2da8b080fe7db9d34f7c56b67bb791
3
+ metadata.gz: 8a37094ad3d7261ff966de95b57241e7fadd5163e4f25303b2ebcfd69f5ebc4b
4
+ data.tar.gz: 7bd37b3b0d4bb6c19d4765bf3892c8f3df0199436313d2c1ae04ba36f72e7bda
5
5
  SHA512:
6
- metadata.gz: 88109a86cc415878a19a461ed56252c42562fd44a40861eebe04f5a65a57c4b59df823851a047e59db49f78a5873e63eb99a27cf91f2e4a1c56646660b5f3e67
7
- data.tar.gz: 9106e4ba0fd8bc5a965fcee3d3e78f1c6cf89ab5f2dcb1c3cbd7762913d6c6382b73c0173985c5f358aca6c58e697bfb78574c7c2c1b88af1b8a2702e89b5aea
6
+ metadata.gz: b79144b9db5b31db8bcdc18f9b6d3abf27115c1ee110c308b0bd2cea4ac07d4a992df03b88234eb30fc299746bcfebb360521c096bb6a198164a36920e05cd5a
7
+ data.tar.gz: dcd974e74516be719fbccba077de410935140b213cabe31a9266c2261df84c358f14a87f4ae2a650f892be83249f98dcf89515dee53489f83989ecefc86db016
data/.gitignore CHANGED
File without changes
data/.rubocop.yml CHANGED
File without changes
data/.solargraph.yml CHANGED
File without changes
data/CODE_OF_CONDUCT.md CHANGED
File without changes
data/Gemfile CHANGED
File without changes
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.1)
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/README.md CHANGED
File without changes
data/Rakefile CHANGED
File without changes
File without changes
@@ -0,0 +1,67 @@
1
+ module Istox
2
+ module CommonHelper
3
+ def self.to_datetime(input)
4
+ return nil if input.blank?
5
+
6
+ begin
7
+ is_numeric = true if Integer input
8
+ rescue StandardError
9
+ false
10
+ end
11
+
12
+ # is unix timestamp
13
+ is_numeric ? Time.at(input.to_i).to_datetime : Time.parse(input)
14
+ end
15
+
16
+ def self.to_boolean(input)
17
+ !(input.blank? || input.to_s.downcase == 'false' || input.to_s.downcase == '0')
18
+ end
19
+
20
+ def self.to_open_struct(model)
21
+ return nil if model.nil?
22
+
23
+ if model.is_a?(Array)
24
+ model.map do |item|
25
+ hash = deep_to_h(item).deep_transform_keys { |key| key.to_s.underscore.to_sym }
26
+ to_recursive_ostruct(hash)
27
+ end
28
+ else
29
+ hash = deep_to_h(model).deep_transform_keys { |key| key.to_s.underscore.to_sym }
30
+ to_recursive_ostruct(hash)
31
+ end
32
+ end
33
+
34
+ def self.to_recursive_ostruct(obj)
35
+ if obj.is_a?(Hash)
36
+ ::Istox::MyOpenStruct.new(obj.map { |key, val| [key, to_recursive_ostruct(val)] }.to_h)
37
+ elsif obj.is_a?(Array)
38
+ obj.map { |o| to_recursive_ostruct(o) }
39
+ elsif obj.is_a?(OpenStruct)
40
+ ::Istox::MyOpenStruct.new(obj)
41
+ else # Assumed to be a primitive value
42
+ obj
43
+ end
44
+ end
45
+
46
+ def self.deep_to_h(obj)
47
+ if obj.is_a?(Array)
48
+ obj.map { |r| deep_to_h(r) }
49
+ elsif obj.is_a?(OpenStruct) || obj.is_a?(Hash) || (obj.methods.include?(:to_h) && obj.present?)
50
+ obj.to_h.transform_values do |v|
51
+ if v.is_a?(OpenStruct) || v.is_a?(Array)
52
+ deep_to_h(v)
53
+ else
54
+ v
55
+ end
56
+ end
57
+ else
58
+ obj
59
+ end
60
+ end
61
+
62
+ def self.get_currency_decimal(currency)
63
+ return 0 if currency&.downcase == 'jpy'
64
+
65
+ 2 end
66
+ end
67
+ end
File without changes
File without changes
File without changes
@@ -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
  # }]
@@ -130,14 +131,15 @@ module Istox
130
131
  next unless ce.present?
131
132
 
132
133
  copy_email_data = email_data.clone
133
-
134
+ log.info "Checking copy email data #{copy_email_data.inspect}"
134
135
  # if it is just email to cc
135
136
  if ce.is_a? String
136
137
  copy_email_data[:email] = ce
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,9 @@ module Istox
146
148
  next unless ce[:email].present?
147
149
 
148
150
  copy_email_data[:email] = ce[:email]
151
+ log.info "Checking copy email data after assigning #{copy_email_data.inspect}"
152
+ log.info "Checking ce #{ce[:params].inspect}"
153
+
149
154
  copy_email_data = copy_email_data.merge(ce[:params])
150
155
  end
151
156
 
File without changes
@@ -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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -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.2.0'.freeze
2
+ VERSION = '0.2.3-50'.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.2.0
4
+ version: 0.2.3.pre.50
5
5
  platform: ruby
6
6
  authors:
7
7
  - Siong Leng
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-03-09 00:00:00.000000000 Z
11
+ date: 2021-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: amazing_print
@@ -494,6 +494,7 @@ files:
494
494
  - lib/istox.rb
495
495
  - lib/istox/constants/error.rb
496
496
  - lib/istox/consumers/blockchain_status_handler.rb
497
+ - lib/istox/helpers/_tmp_07f8e7bcecafce66_common_helper.rb.rb
497
498
  - lib/istox/helpers/blockchain_service.rb
498
499
  - lib/istox/helpers/bunny_boot.rb
499
500
  - lib/istox/helpers/common_helper.rb
@@ -542,11 +543,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
542
543
  version: '0'
543
544
  required_rubygems_version: !ruby/object:Gem::Requirement
544
545
  requirements:
545
- - - ">="
546
+ - - ">"
546
547
  - !ruby/object:Gem::Version
547
- version: '0'
548
+ version: 1.3.1
548
549
  requirements: []
549
- rubygems_version: 3.0.6
550
+ rubygems_version: 3.2.11
550
551
  signing_key:
551
552
  specification_version: 4
552
553
  summary: istox backend shared gem