finrb 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/finrb/yields.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'decimal'
4
4
  require_relative 'errors'
5
+ require_relative 'validation'
5
6
 
6
7
  module Finrb
7
8
  # Money-market yield and interest-rate conversion calculations.
@@ -14,9 +15,9 @@ module Finrb
14
15
  # @example
15
16
  # Finrb::Yields.bdy(d=1500,f=100000,t=120)
16
17
  def self.bdy(d:, f:, t:)
17
- d = Flt::DecNum(d.to_s)
18
- f = Flt::DecNum(f.to_s)
19
- t = Flt::DecNum(t.to_s)
18
+ d = Validation.decimal(d, name: 'dollar discount')
19
+ f = Validation.positive_decimal(f, name: 'face value', error: DomainError)
20
+ t = Validation.positive_decimal(t, name: 'time to maturity', error: DomainError)
20
21
 
21
22
  (d * 360 / f / t)
22
23
  end
@@ -28,10 +29,12 @@ module Finrb
28
29
  # @example
29
30
  # Finrb::Yields.bdy2mmy(bdy=0.045,t=120)
30
31
  def self.bdy2mmy(bdy:, t:)
31
- bdy = Flt::DecNum(bdy.to_s)
32
- t = Flt::DecNum(t.to_s)
32
+ bdy = Validation.decimal(bdy, name: 'bank discount yield')
33
+ t = Validation.positive_decimal(t, name: 'time to maturity', error: DomainError)
34
+ denominator = 360 - (t * bdy)
35
+ raise(DomainError, 'Bank discount yield and time to maturity must imply a positive purchase price.') unless denominator.positive?
33
36
 
34
- (bdy * 360 / (360 - (t * bdy)))
37
+ (bdy * 360 / denominator)
35
38
  end
36
39
 
37
40
  # Convert stated annual rate to the effective annual rate
@@ -44,10 +47,10 @@ module Finrb
44
47
  # @example
45
48
  # Finrb::Yields.ear(0.04,365)
46
49
  def self.ear(r:, m:)
47
- r = Flt::DecNum(r.to_s)
48
- m = Flt::DecNum(m.to_s)
50
+ r = Validation.decimal(r, name: 'stated annual rate')
51
+ m = Validation.positive_decimal(m, name: 'compounding periods', error: DomainError)
49
52
 
50
- ((((r / m) + 1)**m) - 1)
53
+ ((compounding_base(r, m)**m) - 1)
51
54
  end
52
55
 
53
56
  # Convert stated annual rate to the effective annual rate with continuous compounding
@@ -59,7 +62,7 @@ module Finrb
59
62
  # @example
60
63
  # Finrb::Yields.ear_continuous(0.03)
61
64
  def self.ear_continuous(r:)
62
- r = Flt::DecNum(r.to_s)
65
+ r = Validation.decimal(r, name: 'stated annual rate')
63
66
 
64
67
  (r.exp - 1)
65
68
  end
@@ -70,7 +73,7 @@ module Finrb
70
73
  # @example
71
74
  # Finrb::Yields.ear2bey(ear=0.08)
72
75
  def self.ear2bey(ear:)
73
- ear = Flt::DecNum(ear.to_s)
76
+ ear = Validation.decimal_at_least(ear, minimum: -1, name: 'effective annual rate', error: DomainError)
74
77
 
75
78
  (((ear + 1).sqrt - 1) * 2)
76
79
  end
@@ -82,8 +85,8 @@ module Finrb
82
85
  # @example
83
86
  # Finrb::Yields.ear2hpr(ear=0.05039,t=150)
84
87
  def self.ear2hpr(ear:, t:)
85
- ear = Flt::DecNum(ear.to_s)
86
- t = Flt::DecNum(t.to_s)
88
+ ear = Validation.decimal_at_least(ear, minimum: -1, name: 'effective annual rate', error: DomainError)
89
+ t = Validation.positive_decimal(t, name: 'time to maturity', error: DomainError)
87
90
 
88
91
  (((ear + 1)**(t / 365)) - 1)
89
92
  end
@@ -124,18 +127,18 @@ module Finrb
124
127
  # # monthly proportional interest rate which is equivalent to a simple annual interest
125
128
  # Finrb::Yields.eir(r=0.05,p=12,type='p')
126
129
  def self.eir(r:, n: 1, p: 12, type: 'e')
127
- r = Flt::DecNum(r.to_s)
128
- n = Flt::DecNum(n.to_s)
129
- p = Flt::DecNum(p.to_s)
130
+ r = Validation.decimal(r, name: 'annual rate')
131
+ n = Validation.positive_decimal(n, name: 'source compounding periods', error: DomainError)
132
+ p = Validation.positive_decimal(p, name: 'target compounding periods', error: DomainError)
130
133
  type = type.to_s
131
134
 
132
135
  case type
133
136
  when 'e'
134
- eir = (((r / n) + 1)**(n / p)) - 1
137
+ eir = (compounding_base(r, n)**(n / p)) - 1
135
138
  when 'p'
136
139
  eir = r / p
137
140
  else
138
- raise(Error, "type must be 'e' or 'p'")
141
+ raise(ArgumentError, "conversion type must be 'e' (equivalent) or 'p' (proportional)")
139
142
  end
140
143
  eir
141
144
  end
@@ -147,8 +150,8 @@ module Finrb
147
150
  # @example
148
151
  # Finrb::Yields.hpr2bey(hpr=0.02,t=3)
149
152
  def self.hpr2bey(hpr:, t:)
150
- hpr = Flt::DecNum(hpr.to_s)
151
- t = Flt::DecNum(t.to_s)
153
+ hpr = Validation.decimal_at_least(hpr, minimum: -1, name: 'holding period return', error: DomainError)
154
+ t = Validation.positive_decimal(t, name: 'time to maturity', error: DomainError)
152
155
 
153
156
  ((((hpr + 1)**(6 / t)) - 1) * 2)
154
157
  end
@@ -160,8 +163,8 @@ module Finrb
160
163
  # @example
161
164
  # Finrb::Yields.hpr2ear(hpr=0.015228,t=120)
162
165
  def self.hpr2ear(hpr:, t:)
163
- hpr = Flt::DecNum(hpr.to_s)
164
- t = Flt::DecNum(t.to_s)
166
+ hpr = Validation.decimal_at_least(hpr, minimum: -1, name: 'holding period return', error: DomainError)
167
+ t = Validation.positive_decimal(t, name: 'time to maturity', error: DomainError)
165
168
 
166
169
  (((hpr + 1)**(365 / t)) - 1)
167
170
  end
@@ -173,8 +176,8 @@ module Finrb
173
176
  # @example
174
177
  # Finrb::Yields.hpr2mmy(hpr=0.01523,t=120)
175
178
  def self.hpr2mmy(hpr:, t:)
176
- hpr = Flt::DecNum(hpr.to_s)
177
- t = Flt::DecNum(t.to_s)
179
+ hpr = Validation.decimal(hpr, name: 'holding period return')
180
+ t = Validation.positive_decimal(t, name: 'time to maturity', error: DomainError)
178
181
 
179
182
  (hpr * 360 / t)
180
183
  end
@@ -186,8 +189,8 @@ module Finrb
186
189
  # @example
187
190
  # Finrb::Yields.mmy2hpr(mmy=0.04898,t=150)
188
191
  def self.mmy2hpr(mmy:, t:)
189
- mmy = Flt::DecNum(mmy.to_s)
190
- t = Flt::DecNum(t.to_s)
192
+ mmy = Validation.decimal(mmy, name: 'money market yield')
193
+ t = Validation.positive_decimal(t, name: 'time to maturity', error: DomainError)
191
194
 
192
195
  (mmy * t / 360)
193
196
  end
@@ -199,10 +202,10 @@ module Finrb
199
202
  # @example
200
203
  # Finrb::Yields.r_continuous(r=0.03,m=4)
201
204
  def self.r_continuous(r:, m:)
202
- r = Flt::DecNum(r.to_s)
203
- m = Flt::DecNum(m.to_s)
205
+ r = Validation.decimal(r, name: 'nominal rate')
206
+ m = Validation.positive_decimal(m, name: 'compounding periods', error: DomainError)
204
207
 
205
- (m * ((r / m) + 1).log)
208
+ (m * compounding_base(r, m).log)
206
209
  end
207
210
 
208
211
  # Convert a given continuous compounded rate to a norminal rate
@@ -215,10 +218,18 @@ module Finrb
215
218
  # @example
216
219
  # Finrb::Yields.r_norminal(rc=0.03,m=4)
217
220
  def self.r_norminal(rc:, m:)
218
- rc = Flt::DecNum(rc.to_s)
219
- m = Flt::DecNum(m.to_s)
221
+ rc = Validation.decimal(rc, name: 'continuously compounded rate')
222
+ m = Validation.positive_decimal(m, name: 'compounding periods', error: DomainError)
220
223
 
221
224
  (m * ((rc / m).exp - 1))
222
225
  end
226
+
227
+ def self.compounding_base(rate, periods)
228
+ base = (rate / periods) + 1
229
+ raise(DomainError, 'The rate per compounding period must be greater than -1.') unless base.positive?
230
+
231
+ base
232
+ end
233
+ private_class_method :compounding_base
223
234
  end
224
235
  end
data/lib/finrb.rb CHANGED
@@ -1,13 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'finrb/accounting'
4
+ require 'finrb/calendars'
4
5
  require 'finrb/cashflows'
5
6
  require 'finrb/config'
7
+ require 'finrb/day_count'
6
8
  require 'finrb/decimal'
7
9
  require 'finrb/errors'
8
10
  require 'finrb/precision'
9
11
  require 'finrb/ratios'
10
12
  require 'finrb/returns'
13
+ require 'finrb/schedule'
11
14
  require 'finrb/tvm'
12
15
  require 'finrb/version'
13
16
  require 'finrb/yields'
@@ -23,6 +26,7 @@ FinrbError = Finrb::Error
23
26
  # * *rate* represents the interest rate _per period_.
24
27
  module Finrb
25
28
  autoload :Amortization, 'finrb/amortization'
29
+ autoload :FixedRateBond, 'finrb/fixed_rate_bond'
26
30
  autoload :Rate, 'finrb/rates'
27
31
  autoload :Transaction, 'finrb/transaction'
28
32
  end
data/sig/finrb.rbs CHANGED
@@ -42,9 +42,104 @@ module Finrb
42
42
  class DomainError < Error
43
43
  end
44
44
 
45
+ module Calendars
46
+ type convention = :following | :modified_following | :preceding | :modified_preceding | :half_month_modified_following | :nearest | :unadjusted
47
+
48
+ class Base
49
+ CONVENTIONS: Array[Symbol]
50
+
51
+ def initialize: (?additional_holidays: Array[Date], ?removed_holidays: Array[Date]) -> void
52
+ def additional_holidays: () -> Array[Date]
53
+ def removed_holidays: () -> Array[Date]
54
+ def business_day?: (Date) -> bool
55
+ def holiday?: (Date) -> bool
56
+ def holiday_names: (Date) -> Array[String]
57
+ def holidays_between: (Date, Date, ?include_weekends: bool) -> Hash[Date, Array[String]]
58
+ def adjust: (Date, convention: convention) -> Date
59
+ def advance: (Date, business_days: Integer, ?convention: convention) -> Date
60
+ def ==: (untyped) -> bool
61
+ def eql?: (untyped) -> bool
62
+ def hash: () -> Integer
63
+ end
64
+
65
+ class USFederalReserve < Base
66
+ SUPPORTED_START_DATE: Date
67
+ SUPPORTED_END_DATE: Date
68
+ SUPPORTED_DATE_RANGE: Range[Date]
69
+ def name: () -> String
70
+ end
71
+
72
+ class IsraelTase < Base
73
+ SUPPORTED_START_DATE: Date
74
+ SUPPORTED_END_DATE: Date
75
+ SUPPORTED_DATE_RANGE: Range[Date]
76
+ TRADING_WEEK_CHANGE: Date
77
+ def name: () -> String
78
+ end
79
+ end
80
+
81
+ module DayCount
82
+ type convention = :actual_365_fixed | :actual_360 | :actual_actual_icma
83
+ CONVENTIONS: Array[convention]
84
+ DEFAULT: convention
85
+
86
+ def self.year_fraction: (Date, Date, ?convention: convention, ?reference_period_start: Date?, ?reference_period_end: Date?, ?frequency: Integer?) -> decimal
87
+ end
88
+
89
+ class Schedule
90
+ type frequency = :monthly | :quarterly | :semiannual | :annual
91
+ type stub = :none | :short_final
92
+
93
+ FREQUENCY_MONTHS: Hash[frequency, Integer]
94
+ STUB_CONVENTIONS: Array[stub]
95
+
96
+ class Period
97
+ def initialize: (index: Integer, accrual_start_date: Date, unadjusted_payment_date: Date, payment_date: Date, ?stub: stub?) -> void
98
+ def index: () -> Integer
99
+ def accrual_start_date: () -> Date
100
+ def unadjusted_payment_date: () -> Date
101
+ def payment_date: () -> Date
102
+ def stub: () -> stub?
103
+ def short_final_stub?: () -> bool
104
+ end
105
+
106
+ def self.from_months: (start_date: Date, term_months: Integer, ?frequency: frequency, ?stub: stub, ?calendar: Calendars::Base?, ?business_day_convention: Calendars::convention?) -> Schedule
107
+ def initialize: (start_date: Date, maturity_date: Date, ?frequency: frequency, ?stub: stub, ?calendar: Calendars::Base?, ?business_day_convention: Calendars::convention?) -> void
108
+ def start_date: () -> Date
109
+ def maturity_date: () -> Date
110
+ def frequency: () -> frequency
111
+ def stub: () -> stub
112
+ def calendar: () -> Calendars::Base?
113
+ def business_day_convention: () -> Calendars::convention?
114
+ def periods: () -> Array[Period]
115
+ def payment_dates: () -> Array[Date]
116
+ def unadjusted_payment_dates: () -> Array[Date]
117
+ end
118
+
45
119
  class InvalidCashflowError < Error
46
120
  end
47
121
 
122
+ class FixedRateBond
123
+ type price_type = :clean | :dirty
124
+ PRICE_TYPES: Array[price_type]
125
+
126
+ def initialize: (face_value: number, coupon_rate: number, issue_date: Date, maturity_date: Date, ?frequency: Schedule::frequency, ?calendar: Calendars::Base?, ?business_day_convention: Calendars::convention?) -> void
127
+ def face_value: () -> decimal
128
+ def coupon_rate: () -> decimal
129
+ def issue_date: () -> Date
130
+ def maturity_date: () -> Date
131
+ def frequency: () -> Schedule::frequency
132
+ def schedule: () -> Schedule
133
+ def calendar: () -> Calendars::Base?
134
+ def business_day_convention: () -> Calendars::convention?
135
+ def day_count: () -> :actual_actual_icma
136
+ def cashflows: (settlement_date: Date) -> Array[Transaction]
137
+ def accrued_interest: (settlement_date: Date) -> decimal
138
+ def dirty_price: (settlement_date: Date, yield_rate: number) -> decimal
139
+ def clean_price: (settlement_date: Date, yield_rate: number) -> decimal
140
+ def yield_to_maturity: (settlement_date: Date, price: number, ?price_type: price_type, ?guess: number) -> decimal
141
+ end
142
+
48
143
  module Precision
49
144
  def self.money: (number) -> decimal
50
145
  def self.rate: (number) -> decimal
@@ -94,8 +189,14 @@ module Finrb
94
189
  end
95
190
 
96
191
  class Amortization
192
+ type frequency = :monthly | :quarterly | :semiannual | :annual
193
+ type stub = :none | :short_final
194
+
195
+ FREQUENCY_MONTHS: Hash[frequency, Integer]
196
+ STUB_CONVENTIONS: Array[stub]
197
+
97
198
  class Entry
98
- def initialize: (period: Integer, opening_balance: number, payment: number, interest: number, principal: number, additional_payment: number, balloon_payment: number, interest_only: bool, closing_balance: number) -> void
199
+ def initialize: (period: Integer, opening_balance: number, payment: number, interest: number, principal: number, additional_payment: number, balloon_payment: number, interest_only: bool, closing_balance: number, ?date: Date?) -> void
99
200
  def period: () -> Integer
100
201
  def opening_balance: () -> decimal
101
202
  def payment: () -> decimal
@@ -106,15 +207,16 @@ module Finrb
106
207
  def interest_only: () -> bool
107
208
  def interest_only?: () -> bool
108
209
  def closing_balance: () -> decimal
210
+ def date: () -> Date?
109
211
  def ==: (untyped) -> bool
110
212
  def eql?: (untyped) -> bool
111
213
  def hash: () -> Integer
112
- def to_h: () -> Hash[Symbol, Integer | decimal]
214
+ def to_h: () -> Hash[Symbol, Integer | decimal | bool | Date]
113
215
  end
114
216
 
115
217
  def self.payment: (number, number, Integer, ?balloon: number) -> decimal
116
218
 
117
- def initialize: (number, *Rate, ?balloon: number, ?interest_only_periods: Integer, ?origination_fee: number, ?finance_origination_fee: bool) ?{ (Transaction) -> number } -> void
219
+ def initialize: (number, *Rate, ?balloon: number, ?interest_only_periods: Integer, ?origination_fee: number, ?finance_origination_fee: bool, ?start_date: Date?, ?calendar: Calendars::Base?, ?business_day_convention: Calendars::convention?, ?day_count: DayCount::convention, ?frequency: frequency, ?stub: stub) ?{ (Transaction) -> number } -> void
118
220
  def ==: (untyped) -> bool
119
221
  def balance: () -> decimal
120
222
  def balloon: () -> decimal
@@ -128,7 +230,14 @@ module Finrb
128
230
  def principal: () -> decimal
129
231
  def rates: () -> Array[Rate]
130
232
  def schedule: () -> Array[Entry]
233
+ def start_date: () -> Date?
234
+ def calendar: () -> Calendars::Base?
235
+ def business_day_convention: () -> Calendars::convention?
236
+ def day_count: () -> DayCount::convention
237
+ def frequency: () -> frequency
238
+ def stub: () -> stub
131
239
  def additional_payments: () -> Array[decimal]
240
+ def cashflow_yield: (?number) -> Rate
132
241
  def duration: () -> Integer
133
242
  def inspect: () -> String
134
243
  def interest: () -> Array[decimal]
@@ -150,7 +259,7 @@ module Finrb
150
259
  end
151
260
 
152
261
  module TVM
153
- def self.discount_rate: (n: number, pv: number, fv: number, pmt: number, ?type: Integer, ?lower: number, ?upper: number) -> decimal
262
+ def self.discount_rate: (n: number, pv: number, fv: number, pmt: number, ?type: Integer, ?guess: number?, ?lower: number?, ?upper: number?) -> decimal
154
263
  def self.fv: (r: number, n: number, ?pv: number, ?pmt: number, ?type: Integer) -> decimal
155
264
  def self.fv_annuity: (r: number, n: number, pmt: number, ?type: Integer) -> decimal
156
265
  def self.fv_simple: (r: number, n: number, pv: number) -> decimal
@@ -169,7 +278,7 @@ module Finrb
169
278
  module Accounting
170
279
  type inventory_result = { cost_of_goods: decimal, ending_inventory: decimal }
171
280
 
172
- def self.cogs: (uinv: number, pinv: number, units: number | numbers, price: number | numbers, sinv: number, ?method: String) -> inventory_result
281
+ def self.cogs: (uinv: number, pinv: number, units: number | numbers | nil, price: number | numbers | nil, sinv: number, ?method: String | Symbol) -> inventory_result
173
282
  def self.ddb: (cost: number, rv: number, t: Integer) -> { t: Array[Integer], ddb: Array[decimal] }
174
283
  def self.slde: (cost: number, rv: number, t: number) -> decimal
175
284
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: finrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nadir Cohen
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: benchmark-ips
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: bundler-audit
70
84
  requirement: !ruby/object:Gem::Requirement
@@ -248,26 +262,36 @@ extra_rdoc_files:
248
262
  - README.md
249
263
  files:
250
264
  - CHANGELOG.md
265
+ - CONTRIBUTING.md
251
266
  - COPYING
252
267
  - COPYING.LESSER
253
268
  - NOTICE.md
254
269
  - README.md
270
+ - SECURITY.md
255
271
  - lib/finrb.rb
256
272
  - lib/finrb/accounting.rb
257
273
  - lib/finrb/amortization.rb
274
+ - lib/finrb/calendars.rb
275
+ - lib/finrb/calendars/base.rb
276
+ - lib/finrb/calendars/hebrew_calendar.rb
277
+ - lib/finrb/calendars/israel_tase.rb
278
+ - lib/finrb/calendars/us_federal_reserve.rb
258
279
  - lib/finrb/cashflows.rb
259
280
  - lib/finrb/config.rb
260
281
  - lib/finrb/core_ext.rb
261
282
  - lib/finrb/core_ext/array.rb
262
283
  - lib/finrb/core_ext/numeric.rb
284
+ - lib/finrb/day_count.rb
263
285
  - lib/finrb/decimal.rb
264
286
  - lib/finrb/errors.rb
287
+ - lib/finrb/fixed_rate_bond.rb
265
288
  - lib/finrb/numerical/brent.rb
266
289
  - lib/finrb/numerical/rate_search.rb
267
290
  - lib/finrb/precision.rb
268
291
  - lib/finrb/rates.rb
269
292
  - lib/finrb/ratios.rb
270
293
  - lib/finrb/returns.rb
294
+ - lib/finrb/schedule.rb
271
295
  - lib/finrb/transaction.rb
272
296
  - lib/finrb/tvm.rb
273
297
  - lib/finrb/validation.rb
@@ -297,7 +321,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
297
321
  - !ruby/object:Gem::Version
298
322
  version: '0'
299
323
  requirements: []
300
- rubygems_version: 4.0.19
324
+ rubygems_version: 4.0.22
301
325
  specification_version: 4
302
326
  summary: Precision-first financial mathematics for Ruby
303
327
  test_files: []