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 +4 -4
- data/.gitignore +0 -0
- data/.rubocop.yml +0 -0
- data/.solargraph.yml +0 -0
- data/CODE_OF_CONDUCT.md +0 -0
- data/Gemfile +0 -0
- data/Gemfile.lock +33 -22
- data/README.md +0 -0
- data/Rakefile +0 -0
- data/lib/istox/constants/error.rb +0 -0
- data/lib/istox/helpers/_tmp_07f8e7bcecafce66_common_helper.rb.rb +67 -0
- data/lib/istox/helpers/f_math.rb +0 -0
- data/lib/istox/helpers/graphql_client.rb +0 -0
- data/lib/istox/helpers/gruf_listener_hook.rb +0 -0
- data/lib/istox/helpers/messaging.rb +7 -2
- data/lib/istox/helpers/my_open_struct.rb +0 -0
- data/lib/istox/helpers/order_book_price_time.rb +2 -2
- data/lib/istox/helpers/order_book_prorate.rb +0 -0
- data/lib/istox/helpers/rate_limit.rb +0 -0
- data/lib/istox/helpers/regex_helper.rb +0 -0
- data/lib/istox/helpers/result_handler.rb +0 -0
- data/lib/istox/helpers/vault.rb +0 -0
- data/lib/istox/interfaces/chainhub/transaction.rb +0 -0
- data/lib/istox/models/blockchain_receipt.rb +0 -0
- data/lib/istox/models/concerns/blockchain_receipt_query.rb +0 -0
- data/lib/istox/quant/bond.rb +188 -99
- data/lib/istox/version.rb +1 -1
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8a37094ad3d7261ff966de95b57241e7fadd5163e4f25303b2ebcfd69f5ebc4b
|
4
|
+
data.tar.gz: 7bd37b3b0d4bb6c19d4765bf3892c8f3df0199436313d2c1ae04ba36f72e7bda
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
67
|
+
amazing_print (1.3.0)
|
68
68
|
amq-protocol (2.3.2)
|
69
69
|
arel (9.0.0)
|
70
|
-
awesome_print (1.
|
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.
|
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 (
|
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
|
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.
|
123
|
+
ffi (1.15.0)
|
120
124
|
globalid (0.4.2)
|
121
125
|
activesupport (>= 4.2.0)
|
122
|
-
google-protobuf (3.
|
123
|
-
googleapis-common-protos-types (1.0.
|
124
|
-
google-protobuf (~> 3.
|
125
|
-
graphlient (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.
|
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.
|
134
|
-
google-protobuf (~> 3.
|
137
|
+
grpc (1.36.0-universal-darwin)
|
138
|
+
google-protobuf (~> 3.14)
|
135
139
|
googleapis-common-protos-types (~> 1.0)
|
136
|
-
grpc-tools (1.
|
137
|
-
gruf (2.
|
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.
|
174
|
-
ougai (
|
181
|
+
oj (3.11.3)
|
182
|
+
ougai (2.0.0)
|
175
183
|
oj (~> 3.10)
|
176
|
-
paranoia (2.4.
|
177
|
-
activerecord (>= 4.0, < 6.
|
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.
|
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.
|
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
|
data/lib/istox/helpers/f_math.rb
CHANGED
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),
|
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
|
data/lib/istox/helpers/vault.rb
CHANGED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
data/lib/istox/quant/bond.rb
CHANGED
@@ -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,
|
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
|
19
|
-
raise "Invalid coupon_frequency #{
|
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
|
-
@
|
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
|
-
|
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
|
-
|
35
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
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
|
74
|
-
|
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
|
-
(
|
109
|
+
(my_date.next_day + n.month).prev_day
|
77
110
|
else
|
78
|
-
|
111
|
+
my_date.next_day + n.month
|
79
112
|
end
|
80
113
|
end
|
81
114
|
|
82
|
-
def is_month_end?(
|
83
|
-
|
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
|
-
|
131
|
-
|
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
|
-
|
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
|
-
|
138
|
-
|
139
|
-
|
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
|
-
#
|
142
|
-
#
|
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
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
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
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
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
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
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
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
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
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.
|
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-
|
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:
|
548
|
+
version: 1.3.1
|
548
549
|
requirements: []
|
549
|
-
rubygems_version: 3.
|
550
|
+
rubygems_version: 3.2.11
|
550
551
|
signing_key:
|
551
552
|
specification_version: 4
|
552
553
|
summary: istox backend shared gem
|